<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[BI Diaries -  Nālaka Wanniarachchi]]></title><description><![CDATA[This blog delivers insights and tutorials on Microsoft Fabric, Power BI, data engineering, analytics, etc with actionable strategies for business intelligence professionals.]]></description><link>https://bidiaries.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1731079132479/12e335ee-0171-4ec8-940e-0632ce21e110.jpeg</url><title>BI Diaries -  Nālaka Wanniarachchi</title><link>https://bidiaries.com</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 19:19:03 GMT</lastBuildDate><atom:link href="https://bidiaries.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Machine Learning on Databricks for Data Engineers]]></title><description><![CDATA[I built two models on Databricks from scratch — one predicts a used bike's price, and one predicts whether a loan will default. Both follow the same pattern: raw file → feature table → training → save]]></description><link>https://bidiaries.com/machine-learning-on-databricks-for-data-engineers</link><guid isPermaLink="true">https://bidiaries.com/machine-learning-on-databricks-for-data-engineers</guid><category><![CDATA[Databricks]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[mlops]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Wed, 02 Sep 2026 11:15:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/6c869239-7466-402c-be2a-e6fffa54f43d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built two models on Databricks from scratch — one predicts a used bike's price, and one predicts whether a loan will default. Both follow the same pattern: raw file → feature table → training → saved model → predictions. The useful lesson came from the loan model: nearly 90% accuracy sounded good, but the model caught almost none of the defaults. Accuracy alone hid the result that mattered.</p>
<blockquote>
<p>TL;DR</p>
<ul>
<li><p>Two models on a fake e-bike shop dataset: price (regression) and loan default (classification).</p>
</li>
<li><p>Linear Regression had the lowest held-out MAE for price - with only 817 usable bikes, the simple model performed best on the primary KPI.</p>
</li>
<li><p>The default classifier was close to <strong>90% accurate</strong> but caught only about <strong>1% of defaults</strong>.</p>
</li>
<li><p>Retraining with class balancing lowered accuracy but caught roughly <strong>70% of defaults</strong>.</p>
</li>
<li><p>The worked example at the end explains why accuracy is not enough when the important rows are rare.</p>
</li>
</ul>
</blockquote>
<h3>The method, end to end</h3>
<p>Every model in this project follows the same six steps. each phase below maps back to it:</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/b464778a-f986-4c51-9cc2-0296e2b670c2.png" alt="One pipeline, run twice - once per question" style="display:block;margin:0 auto" />

<p>Two questions, one pipeline shape. <strong>What will this bike sell for?</strong> → regression (Phase 4). <strong>Will this loan default?</strong> → classification (Phase 5). Same steps, run twice.</p>
<h3>Phase 1 · What a model actually is</h3>
<p>A model is a function learned from historical examples instead of a set of rules written by hand.</p>
<p>You show it past rows where you already know the answer ,this bike sold for £2,000, that one for £500 ;and it finds a pattern. For a new row, it applies that pattern. In code, that's it:</p>
<pre><code class="language-python">model.fit(examples, answers)     # here's what happened before
model.predict(new_case)          # now guess this one
</code></pre>
<p><strong>Supervised</strong> = you have the answer column already (paid back or not).</p>
<p><strong>Unsupervised</strong> = no answer column, just "find groups" or "find weird rows." This project is supervised only.</p>
<p>A data engineer may not choose the final algorithm, but still needs to understand what goes into it, what comes out, and how it will run in production.</p>
<h3>The model families used in this project</h3>
<p>These are the model families the notebooks actually compare:</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/a7a50d28-7d4c-4208-a359-9014b8621968.png" alt="" style="display:block;margin:0 auto" />

<p>K-Means, DBSCAN and Isolation Forest are useful unsupervised tools, but they are not part of these two Pedalflow pipelines, so they are outside this walkthrough.</p>
<h3>Regression vs classification - The rule that saves you</h3>
<p>"Regression" means number. "Classification" means yes/no. Except <strong>Logistic Regression</strong>, which does yes/no despite the name .</p>
<p>Both Linear and Logistic Regression sum weighted inputs. Linear outputs a number. Logistic squashes the sum to 0–1 so it reads as a probability.</p>
<blockquote>
<p>The one rule that actually matters</p>
<p><strong>Look at the target column, not the algorithm name.</strong> <br />Number target → regression. Yes/no target → classification.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/3374244e-1fc7-45a8-99e3-3dd6f16a0a2a.png" alt="" style="display:block;margin:0 auto" />

<p><code>predicted_price_gbp</code> could be 1432.57, so it is regression. The loan result is class 0 or 1, so it is classification. The associated probability tells us how strongly the model leans toward class 1.</p>
<p>You pick the question by picking the target column. The algorithm follows.</p>
<h2>Phase 2 · Meet Pedalflow</h2>
<p><strong>You are here:</strong> raw CSVs. Nothing cleaned yet.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/142e84a6-7d11-4d91-b2e2-30f6849cbc04.png" alt="" style="display:block;margin:0 auto" />

<p>Pedalflow is a fake e-bike shop I made up for this walkthrough. Two datasets: used-bike resale (regression) and loan applications/ebike_finance (classification).</p>
<h3>Spreadsheet 1: a used bike for sale</h3>
<p>One row = one bike. Target: <strong>Resale_Price_GBP</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/f76e1eaf-6ea8-41d5-813b-8e6e81b56fe9.png" alt="" style="display:block;margin:0 auto" />

<p>Nearly new, good condition, sold for £2,413. Across the file, prices run from £120 to about £11,000, with a typical bike around £1,174.</p>
<h3>Spreadsheet 2: someone asking to finance a bike</h3>
<p>One row = one loan application, captured <em>before</em> approval. Target: <strong>IsDefault</strong> (0 = paid, 1 = defaulted). Every feature must be knowable at decision time — no future payment history.</p>
<p>~10% of loans default. That imbalance drives everything in Phase 5.</p>
<p>All data is synthetic and generated from a fixed seed. The raw rows are reproducible, but exact model metrics can still move with library versions and time-dependent features such as <code>Bike_Age</code>, which the notebook calculates from the current year.Phase 3 · Loading and cleaning the data</p>
<h3>Bike file (regression)</h3>
<p>Load — nothing new if you read CSVs for a living:</p>
<pre><code class="language-python">df = spark.read.csv("ebike_resale.csv", header=True, inferSchema=True)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/84f79654-f2d7-448b-bada-75f84a19bbec.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/9040e61d-e837-491c-bc03-c92448a8dc2b.png" alt="" style="display:block;margin:0 auto" />

<p>900 bikes in, blanks and all. Four gaps in seven rows - and those blanks are the next decision.</p>
<p>Three cleaning steps:</p>
<ol>
<li>Nulls. Four gaps in seven sample rows. Easy fix: dropna() — 900 rows become 817. But first check: are nulls random, or do cheap bikes skip inspection? Dropping blindly can bias the training set.</li>
</ol>
<p><strong>900 rows in → 817 rows saved → 83 rows dropped for missing values.</strong></p>
<p>One line of code, nine per cent of the data gone. Saved as pedalflow.ml.ebike_features so every later step reads the same 817 rows.</p>
<ol>
<li><p>Feature engineering. Replaced Year_Manufactured with Bike_Age. "Made in 2019" is weak; "7 years old" is useful. Domain knowledge beats algorithm choice here.</p>
</li>
<li><p>Scaling. Battery capacity is in hundreds, while gears are under 20. This matters for distance-based models such as KNN and can matter for some linear or gradient-based models. Tree models are much less sensitive to scale. Dividing each numeric column by its standard deviation keeps the comparison fair across the model families used here.</p>
</li>
</ol>
<blockquote>
<p>Evaluation limitation</p>
<p>The current notebook calculates standard deviations before the train/test split. That lets the held-out data influence preprocessing slightly. A production pipeline should fit scaling parameters on the training rows only, then reuse those exact parameters for test and inference data.</p>
</blockquote>
<p>No text columns in this file - no encoding needed. The target column (<code>Resale_Price_GBP</code>) stays out of the feature table.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/7de81346-5bcd-4ee7-824d-5351000730b8.png" alt="" style="display:block;margin:0 auto" />

<p>Scaled values aren't human-readable - that's fine. The model doesn't need pounds and years; it needs consistent numbers.</p>
<p><strong>Result: 817 rows, 11 columns, saved as</strong> <a href="http://pedalflow.ml"><code>pedalflow.ml</code></a><code>.ebike_features</code><strong>.</strong></p>
<h3>Loan file (classification)</h3>
<pre><code class="language-plaintext">df = spark.read.csv("ebike_finance.csv", header=True, inferSchema=True)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/815b35b7-36dd-4ee2-9731-5577a18424b5.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/1bfca565-b6d7-463c-be43-8e784da01e24.png" alt="" style="display:block;margin:0 auto" />

<p>Six of the nine columns here are words, and not one of them means anything to a model until it becomes a number. IsDefault is the answer we're trying to learn - 0 means they paid.</p>
<p>Different problems:</p>
<ol>
<li><p>Nulls: none. Same dropna() line, zero rows lost. Don't drop nulls by reflex — check first.</p>
</li>
<li><p>No scaling, no new columns in this notebook.</p>
</li>
<li><p><strong>Encoding: seven text columns → numbers. Models do arithmetic, not English.</strong></p>
</li>
</ol>
<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">What do 0, 1, 2 and 3 mean? They are category codes, not weights or scores. For example, the notebook currently maps Married, Single and Divorced to three numbers based on how often each value appears. A code of 2 does not mean “twice as important” as a code of 1.</mark></p>
<p><strong>There is one important catch</strong>: scikit-learn receives these columns as ordinary numbers. A Random Forest can therefore split a column with a rule such as Marital_Status_idx &lt;= 1.5. That introduces an artificial order even though marital status has no natural ranking. The numbers are intended as IDs, but the current models can still use their numeric order.</p>
<blockquote>
<p><strong>A limitation in the current notebook</strong></p>
<p>The frequency encoding is a convenient demo shortcut, not the design I would use in production. Education should use an explicit order. Unordered columns such as employment type, marital status and plan purpose should normally use one-hot encoding, or a model configured to handle categorical values directly. The mapping should be fitted on training data, saved and reused so test data cannot influence it and new data cannot silently change the codes.</p>
</blockquote>
<p>Binary Yes/No columns can safely use a fixed 0/1 mapping. Existing numeric columns such as age, income and credit score stay numeric. The target, <code>IsDefault</code>, is kept out of the feature table.</p>
<p>The loan notebook does not scale its numeric features. That is acceptable for the tree-based models, but it puts KNN at a disadvantage because large-range columns such as income dominate its distance calculation. Treat the KNN result as illustrative unless it is rerun with suitable preprocessing.</p>
<h4>Scaling ≠ encoding</h4>
<p>Scaling (bikes): shrinks numbers, keeps order. Encoding (loans): assigns arbitrary integers to categories. Different operations, different files.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/1a02d884-9bb5-4899-97f9-b91791f26c75.png" alt="" style="display:block;margin:0 auto" />

<p>Words became numbers; the label stayed out. <strong>Output: 40,000 rows →</strong> <a href="http://pedalflow.ml.finance"><code>pedalflow.ml.finance</code></a><code>_features</code><strong>.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/afb9e48a-dd2a-40c6-a67a-49c35cf82c9e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/31a24377-d0f8-4aec-a190-98933dbdda74.png" alt="" style="display:block;margin:0 auto" />

<h3>The two side by side</h3>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/1f87128e-331d-460f-8d65-c882d25ef677.png" alt="" style="display:block;margin:0 auto" />

<p>Save feature tables to Unity Catalog - not notebook variables. Training and scoring must read identical columns built the same way, or the model drifts silently.</p>
<h2>Phase 4 · Train: predict bike price</h2>
<p><strong>You are here:</strong> <code>ebike_features</code> → pick a winning regressor.</p>
<p>The feature table deliberately excludes the answer. The training notebook reads <code>Bike_ID</code> and <code>Resale_Price_GBP</code> from the source file, then uses a Databricks <code>FeatureLookup</code> to join those labels back to <code>ebike_features</code> by <code>Bike_ID</code>. The loan notebook follows the same pattern with <code>AgreementID</code> and <code>IsDefault</code>.</p>
<h3>How do you measure error on rows you haven't seen?</h3>
<p>Hold out a test set. Same idea as validating a pipeline against a known-good sample:</p>
<pre><code class="language-plaintext">817 bikes, all with known sale prices
  |
  +-- 653 bikes  TRAINING   model sees the prices, learns from them
  |
  +-- 164 bikes  TESTING    prices hidden from the model completely
</code></pre>
<p>653 train, 164 test. Model never sees test labels during training. Loans split 32k/8k with <code>stratify=y</code> so ~10.6% default rate holds in both halves.</p>
<h3>Regression KPIs: how we choose the best price model</h3>
<p>In ML these are usually called <strong>evaluation metrics</strong>. From a delivery point of view, they are the KPIs used to decide whether one model is better than another. The primary KPI should be chosen from the business problem <em>before</em> comparing models; otherwise it is easy to pick whichever number makes a preferred model look good.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/138288c9-159a-4539-bafe-fd5988809882.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Why MAE is primary:</strong> the business question is “how many pounds away was the estimate?” MAE answers that directly and gives equal importance to overpricing and underpricing. RMSE is checked as a guardrail because the dataset contains a few high-value collector bikes, where a large miss matters. R² confirms that the model adds value over a simple mean-price baseline.</p>
<p><strong>Selection rule:</strong> compare every model on the same 164 held-out bikes, select the lowest MAE, then confirm that RMSE is acceptable and R² is positive. Using that rule, Linear Regression is the selected model for this run.</p>
<blockquote>
<p><strong>Why MAE is primary:</strong> the business question is “how many pounds away was the estimate?” MAE answers that directly and gives equal importance to overpricing and underpricing. RMSE is checked as a guardrail because the dataset contains a few high-value collector bikes, where a large miss matters. R² confirms that the model adds value over a simple mean-price baseline.</p>
<p><strong>Selection rule:</strong> compare every model on the same 164 held-out bikes, select the lowest MAE, then confirm that RMSE is acceptable and R² is positive. Using that rule, Linear Regression is the selected model for this run.</p>
</blockquote>
<h3>Seven tools, same 817 bikes</h3>
<p>Train seven algorithms, log metrics to MLflow - same loop as any batch job you'd audit later:</p>
<pre><code class="language-python">model_variants = {
    "Linear Regression": LinearRegression(),
    "Decision Tree": DecisionTreeRegressor(),
    "Random Forest": RandomForestRegressor(),
    "XGBoost": xgb.XGBRegressor(),
    "LightGBM": lgb.LGBMRegressor(),
    # ...and two more
}

for name, model in model_variants.items():
    model.fit(X_train, y_train)              # learn from the 653
    preds = model.predict(X_test)             # guess the 164
    mae = mean_absolute_error(y_test, preds)  # how far off, on average
    r2 = r2_score(y_test, preds)              # score against Larry
    mlflow.log_metric("mae", mae)             # store the result
    mlflow.log_metric("r2", r2)
</code></pre>
<p>Seven tools, same split. Log everything - otherwise you're comparing screenshots from memory.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/f8ee2b92-f795-497a-8b9b-ab4170ada599.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Linear Regression had the lowest MAE on this held-out split.</strong> The synthetic price formula includes non-linear depreciation and a few collector-bike outliers, so this result does not prove that bike pricing is truly linear. It shows that, with only 817 usable rows and this preprocessing, the simpler model generalised best according to the chosen KPI. LightGBM was close (£174 vs £164), while the single Decision Tree generalised poorly (R² 0.146).</p>
<p><strong>MAE</strong> is the average absolute error in pounds. <strong>R²</strong> compares squared prediction error with a baseline that always predicts the test-set mean: 1 is perfect, 0 matches that baseline, and a negative value is worse.</p>
<h3>Meet Larry</h3>
<p><strong>Larry</strong> always predicts the mean price (£1,174). R² = 0.725 means our model beats Larry by a wide margin. If you can't beat Larry, you don't have a model — you have an expensive average.</p>
<p><code>Side note: Random Forest ranked Distance_To_Service_Hub_KM third in importance — a column I invented with no real link to price. Don't trust feature-importance charts blindly.</code></p>
<p>Every run logged automatically. The straight line wins — but look how close LightGBM gets.</p>
<h2>Phase 5 · Train: predict loan default</h2>
<p><strong>You are here:</strong> <code>finance_features</code> → classifier. Same train/test split pattern as Phase 4.</p>
<p>About 90% of the loans are repaid. A baseline that predicts “no default” for every application is therefore correct about 89.4% of the time, even though it catches no defaults:</p>
<pre><code class="language-python">def guess(applicant):
    return "will pay it back"   # every single time
</code></pre>
<p>When the important class is rare, accuracy is not enough on its own.</p>
<h3>Classification KPIs: how we choose the useful model</h3>
<p>The KPI comes from the cost of making the wrong decision. In this example, approving a loan that later defaults is more costly than sending a good application for additional review. That makes <strong>recall for the default class</strong> the primary KPI.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/1c20c692-a296-4dea-ae27-28ab5397ea58.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/feb6f8ec-d270-498f-8a0e-bab1cab0afad.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Selection rule:</strong> first require recall to improve substantially over the 0% baseline. Then check precision and the confusion matrix to confirm that the number of false alarms is operationally affordable. Finally, check ROC-AUC to confirm that the probability ranking contains useful signal.</p>
<blockquote>
<p>Important limitation</p>
<p>The demo does not define a maximum review workload or an agreed financial cost for false positives and false negatives. For that reason, the balanced Random Forest is the <strong>selected demonstration model</strong>, not a production-approved winner. A production team should agree those limits before training and encode them as an acceptance gate.</p>
</blockquote>
<p>The notebook trains on 32,000 rows and tests on 8,000 held-out rows. Of those test rows, 846 are defaults. It compares two Random Forest models:</p>
<pre><code class="language-plaintext">default_model = RandomForestClassifier(
    n_estimators=100,
    max_depth=8,
    random_state=42,
)

balanced_model = RandomForestClassifier(
    n_estimators=100,
    max_depth=8,
    class_weight="balanced",
    random_state=42,
)
default_model.fit(X_train, y_train)
balanced_model.fit(X_train, y_train)
</code></pre>
<p><code>class_weight="balanced"</code> gives more influence to the rare default rows during training. This produces a newly trained model; it is not the same fitted model with a different display threshold.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/1ccfb5c6-b87c-4e1f-b845-7204bf18d6a8.png" alt="" style="display:block;margin:0 auto" />

<p>The exact figures are logged in MLflow, but the direction is clear: the default model catches only a handful of the 846 defaults, while the balanced model catches roughly seven out of ten. Its overall accuracy is lower because it also sends more good applications for review.</p>
<blockquote>
<p>How selection works in the current notebooks</p>
<p>The notebooks log every candidate's metrics, then manually set <code>best_name</code> to Linear Regression or the balanced Random Forest. They do not automatically calculate a winner. That is acceptable for a transparent demo; a production workflow should turn the agreed primary KPI and guardrails into a repeatable selection and approval step.</p>
</blockquote>
<h3>The four boxes</h3>
<p>Every prediction lands in one of four groups:</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/e08926ca-1653-4eab-bc7a-77d335bb0ee5.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Precision</strong> asks: of the applications we flagged, how many really defaulted? <strong>Recall</strong> asks: of all applications that defaulted, how many did we catch? Improving recall usually creates more false alarms, so the final operating point is a business decision.</p>
<blockquote>
<p>Worth remembering</p>
<p>For this use case, a lower-accuracy model can be more useful if it catches substantially more defaults. Report the confusion matrix, precision and recall alongside accuracy.</p>
</blockquote>
<h3>AUC: does the model rank risk well?</h3>
<p>AUC asks whether a randomly chosen default is usually scored as riskier than a randomly chosen non-default. It uses probability scores rather than one fixed approval threshold. In this experiment both Random Forest variants are around 0.80 AUC, so both contain useful ranking signal even though their class predictions differ sharply.</p>
<p>AUC is useful, but it does not choose the business threshold for you. Log AUC together with precision, recall and the confusion matrix.</p>
<h2>Phase 6 · Save and score</h2>
<p><strong>You are here:</strong> models selected → save the fitted model → load it from another notebook → score sample rows.</p>
<h3>What the notebooks actually save</h3>
<p>The training loop logs metrics to MLflow and attempts to register every non-baseline candidate with <code>fe.log_model()</code>. On this workspace, model artifact upload can fail even when the metrics were recorded successfully. The working handoff therefore saves only the selected model to a Unity Catalog Volume with <code>joblib</code>:</p>
<pre><code class="language-plaintext">payload = {
    "model": best_model,
    "model_name": best_name,
    "feature_names": list(X_train.columns),
}
joblib.dump(payload, "/Volumes/pedalflow/ml/data/models/model.joblib")
</code></pre>
<p>Saving <code>feature_names</code> matters. The inference notebook uses that list to select the same columns in the same order and fails clearly if a required feature is missing.</p>
<blockquote>
<p>Registry versions</p>
<p>If registration succeeds, a higher version number only means “saved later.” It does not mean the model is better. Promotion should be based on logged metrics and an explicit approval process.</p>
</blockquote>
<h3>What inference currently does</h3>
<p>Each <code>03_inference.ipynb</code> notebook loads the saved <code>joblib</code> payload, reads selected rows from the feature table, restores the expected column order and displays predictions. The classification notebook also displays <code>probability_of_default</code>.</p>
<blockquote>
<pre><code class="language-python">payload = joblib.load(MODEL_PATH)
model = payload["model"]
expected = payload["feature_names"]

features = spark.table(FEATURE_TABLE).toPandas()
X = features[expected]
features["prediction"] = model.predict(X)
</code></pre>
</blockquote>
<p>This proves that the artifact can be handed from training to inference. It is not yet a production batch pipeline: the current notebooks score a small sample and do not write <code>ebike_predictions</code> or <code>finance_predictions</code> tables. Real-time serving is also not implemented here.</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/bd408f44-fc65-4f53-a340-e97d78fc6d4d.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/42f808b2-ee0e-4c10-9675-fb70f05b59df.png" alt="" style="display:block;margin:0 auto" />

<h2>What got built</h2>
<p>Pulling the map from the top of this post back out, filled in with what actually exists in the workspace right now:</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/14c52b04-e8eb-419d-9f13-f6441f7f5969.png" alt="" style="display:block;margin:0 auto" />

<p>Two takeaways: the selected model is not always the fanciest algorithm, and the primary KPI must match the business question—not whichever number looks best after training.</p>
<h2>Worked example: why 89.5% is not enough</h2>
<p>Pipeline version — 8,000 rows, 846 corrupted, 7,154 clean:</p>
<img src="https://cdn.hashnode.com/uploads/covers/665d71d9e9e9ee15e2bfe07a/232d8b25-e865-4b29-9541-61bf8df87dd2.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 1:</strong> Mark everything "fine" → 7,154/8,000 = 89.4% accurate, 0 catches. That's your floor.</p>
<p><strong>Step 2:</strong> The default trained checker remains close to 89.5% accurate but catches only about 1% of corrupted rows. For the purpose of finding corruption, it barely improves on doing nothing.</p>
<p><strong>Step 3:</strong> Retrain with class balancing. Accuracy falls to roughly 74%, but recall rises to roughly 70%. “Worse” accuracy, much better coverage of the problem.</p>
<p><strong>Step 4:</strong> Same trap as a dashboard showing "99% rows OK" while missing the duplicate-key failures you built the job to find.</p>
<blockquote>
<p>Rule</p>
<p>Rare target → ask <strong>of the bad rows, how many did we catch?</strong> Not "% correct overall."</p>
</blockquote>
<h2>Takeaways for your team</h2>
<p>In standup terms:</p>
<ul>
<li><p><strong>Feature engineering is most of the work.</strong> Nulls, encoding, scaling — same as building a gold table. <a href="http://model.fit"><code>model.fit</code></a><code>()</code> is two lines.</p>
</li>
<li><p><strong>Feature table = contract.</strong> Same columns, same order, same logic for train and score. Save to Unity Catalog.</p>
</li>
<li><p><strong>Log every run.</strong> MLflow metrics or you're arguing from notebook output.</p>
</li>
<li><p><strong>Version ≠ best.</strong> If registry upload succeeds, select versions by metrics and approval status—not by the highest number.</p>
</li>
<li><p><strong>Always report the baseline.</strong> Larry for regression, "always approve" for classification.</p>
</li>
<li><p><strong>Pin reproducibility.</strong> <code>random_state=42</code>, <code>stratify=y</code> — same reason you pin package versions.</p>
</li>
<li><p><strong>Batch vs real-time is infra, not ML.</strong> Know which you can support before promising it.</p>
</li>
</ul>
<p>Code, data, and reproducible numbers: <a href="https://github.com/nalakan/Machine-Learning-on-Databricks-for-Data-Engineers">Machine-Learning-w-DBX.</a></p>
<p>Thanks for Reading .Keep in touch 👍</p>
]]></content:encoded></item><item><title><![CDATA[Containerized PowerBI Blue🔵🟢Green Deployment solution with Fabric CLI, Azure DevOps Pipelines + Docker]]></title><description><![CDATA[💡
TL;DR: This article presents a complementary approach to Power BI deployments using blue-green deployment strategies with Docker containers, by leveraging existing classic Power BI deployment pipelines along with Azure DevOps pipelines, Fabric CLI...]]></description><link>https://bidiaries.com/blue-green-deployment-for-power-bifabric-with-docker-and-azure-devops-cicd</link><guid isPermaLink="true">https://bidiaries.com/blue-green-deployment-for-power-bifabric-with-docker-and-azure-devops-cicd</guid><category><![CDATA[Docker]]></category><category><![CDATA[docker images]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[fabric]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Microsoft365]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[Azure]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[CI/CD pipelines]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Thu, 25 Sep 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757698509201/a9cb3f6a-6b5e-4efc-91e6-c4a9c4cec7fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>TL;DR:</strong> This article presents a <strong>complementary approach</strong> to Power BI deployments using blue-green deployment strategies with Docker containers, <strong>by leveraging existing classic Power BI deployment pipelines</strong> along with Azure DevOps pipelines, Fabric CLI, and Power BI REST APIs. The solution creates parallel production environments, enabling zero-downtime deployments and instant rollbacks while maintaining data consistency across development, test, and production workspaces. Although the technical architecture may seem complex, the actual implementation for end users is straightforward-simply modify the pipeline parameters in Azure DevOps variable groups and execute the DevOps pipeline to deploy across all environments automatically.</div>
</div>

<h2 id="heading-understanding-blue-green-deployments">Understanding Blue-Green Deployments</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758859818916/cb1c5b88-bd9b-4344-8a20-efc2d920c460.png" alt class="image--center mx-auto" /></p>
<p>Blue-green deployment is a technique that reduces downtime and risk by running two identical production environments called "Blue" and "Green." At any time, only one environment serves live traffic while the other remains idle. In traditional web applications and Azure Function Apps, this works by maintaining two complete infrastructure stacks with load balancers switching traffic between them.(Azure web app deployment slots are shown here below)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757524824942/d22df5d4-3ee1-4b92-b3c3-2ef46b90b75b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-this-matters-creating-parallel-production-environments">Why This Matters: Creating Parallel Production Environments</h2>
<p><strong>The Azure web app/function app Deployment Model Applied to Power BI</strong></p>
<p>Azure App Services and Function Apps achieve zero-downtime deployments through deployment slots - maintaining two identical production environments where one serves live traffic while the other stands ready for updates. When deployment is complete, traffic switches seamlessly between environments, providing instant rollback capability.PowerBI currently missing native deployment slots, but the operational requirement remains the same. create parallel production instances that allow safe updates without impacting live business operations.</p>
<p><strong>Building Production Twins for Power BI</strong></p>
<p>This solution implements the deployment slot concept within PowerBI workspaces by creating identical copies of production assets:</p>
<ul>
<li><p><strong>Active Production</strong>: Current reports and semantic models serving business users</p>
</li>
<li><p><strong>Staging Production</strong>: Parallel versions with deployment prefixes, ready for validation and switchover</p>
</li>
</ul>
<p>This approach mirrors Azure's blue-green deployment pattern, adapted for PowerBI's workspace-centric architecture.</p>
<p><strong>The Core Business Value</strong></p>
<p>Organizations achieve the similar operational benefits that Azure deployment slots provide for web applications:</p>
<ul>
<li><p><strong>Continuous Availability</strong>: Production reports remain accessible throughout deployment cycles, eliminating service interruptions during updates.</p>
</li>
<li><p><strong>Risk Mitigation</strong>: New versions undergo complete validation in production-identical environments before user exposure, reducing deployment-related incidents.</p>
</li>
<li><p><strong>Rapid Recovery</strong>: Rollback operations execute immediately by redirecting users to previous versions, minimizing business impact when issues arise.</p>
</li>
<li><p><strong>Deployment Confidence</strong>: Teams can execute updates during business hours with the assurance that failures won't disrupt operations.</p>
</li>
</ul>
<p><strong>What We Achieve</strong></p>
<p>This implementation transforms PowerBI deployments from high-risk process into reliable, automated operations. Business users experience consistent service availability while IT teams gain the operational maturity and deployment confidence that modern DevOps practices provide for other enterprise applications.The <strong>parallel environment</strong> approach brings enterprise-grade deployment practices to PowerBI, ensuring analytics infrastructure operates with the same reliability standards as mission-critical business applications.Below mention high level flow of PowerBI artifacts movement.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757524391938/f94e1566-cadb-453d-b752-6b4d992c871d.png" alt class="image--center mx-auto" /></p>
<p>Our approach adapts blue-green deployment principles to PowerBI's constraints using prefix-based naming conventions. Instead of separate infrastructure, we create parallel versions of reports and semantic models within the same or different workspaces, distinguished by prefixes.(If this is unclear, continue reading through the entire article)</p>
<p><strong>The strategy works as follows:</strong></p>
<ul>
<li><p><strong>Blue Environment (Current Production) marked as 1</strong> : Reports and semantic models with standard production names (e.g., "Prod Model A", "Prod Report A").</p>
</li>
<li><p><strong>Green Environment (New Version)</strong>: <mark>Based on production workspace artifacts (1), the solution creates reports and semantic models with environment-specific prefixes (e.g., Dev-Model A, Dev-Report A(2), Test-Model A, Test-Report A(3), Prod_New_Model A, Prod_New_Report A (4)) by copying artifacts and binding them to their respective workspaces.</mark></p>
</li>
</ul>
<p>When deployment completes successfully, the green environment becomes the new production standard, and the blue environment serves as a rollback option.</p>
<h2 id="heading-solution-architecture">🏗️ Solution Architecture</h2>
<p>The solution combines several technologies to create a robust, automated deployment pipeline:</p>
<ul>
<li><p><strong>Docker Containers</strong>: Provide consistent deployment environments and easy scalability across multiple target environments.</p>
</li>
<li><p><strong>Azure DevOps Pipelines</strong>: Orchestrate the build and deployment process with proper credential management.</p>
</li>
<li><p><strong>Fabric CLI</strong>: Handle workspace operations like copying reports and semantic models.</p>
</li>
<li><p><strong>PowerBI REST APIs</strong>: Manage report-to-dataset bindings and resolve object identifiers.(I initially attempted to implement this functionality using Fabric CLI's set (<a target="_blank" href="https://microsoft.github.io/fabric-cli/examples/item_examples/#rebind-report-to-semantic-model">fab set</a>) command, but came no success and reverted to the basics)</p>
</li>
<li><p><strong>Service Principal Authentication</strong>: Enable secure, automated access to PowerBI workspaces.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758860370586/eaeb350c-13cf-4c4d-a316-2703f721d327.gif" alt class="image--center mx-auto" /></p>
<p>The above image(click on the image to zoom) explains flow that demonstrates how code changes flow from development through testing to production environments using containerization and automated deployment strategies.</p>
<p>The diagram illustrates a multi-stage deployment process that begins with main branch code changes being processed through Azure DevOps and pushed to Docker Hub as container images. The pipeline then supports both manual and automated deployment paths, where images are pulled and deployed across three distinct environments: Development, Testing, and Production.</p>
<p>The workflow shows how containers are dynamically created using <code>docker run --rm</code> commands for each environment, with built-in authentication, copying, and rebinding processes. Each container follows a complete lifecycle from creation to destruction, with the system maintaining separate workspaces for different environments. The architecture also incorporates PowerBI/Fabric production components and DevOps variable libraries for configuration management, demonstrating a robust continuous integration and deployment (CI/CD) strategy that ensures code quality and environment isolation throughout the software delivery lifecycle.</p>
<h2 id="heading-two-main-deployment-phases">Two Main Deployment Phases</h2>
<p>The architecture consists of two distinct phases that optimize both developer productivity and end-user simplicity:</p>
<p><strong>Phase 1: Image Building (One-Time Setup , already cooked 🍳&amp; I am explaining how I did and what it does)</strong> When developers push changes to the main branch of the Prod PowerBI devops repo, Azure DevOps triggers an automated build process that creates a Docker image containing all deployment scripts, Fabric CLI tools, and dependencies. The completed <mark>image is stored in </mark> <a target="_blank" href="https://hub.docker.com/repository/docker/nalakan/pbi_blue_green_dep_v1/general"><mark>Docker Hub</mark></a>, ready for use across all environments.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758865242107/2478873e-14ac-4083-9ec1-25fae0dd0000.png" alt class="image--center mx-auto" /></p>
<p><strong>Phase 2: Deployment Execution (Adhoc Operations) 🎯</strong> This is where end users interact with the system through two simple steps to utilize the image:</p>
<ul>
<li><strong>Configure Parameters</strong>: Update Azure DevOps variable groups with workspace and report specifications &amp; Service Principal credentials</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758865391001/78bba4c7-7c64-42a3-ade2-d7e73480d183.png" alt class="image--center mx-auto" /></p>
<ul>
<li><strong>Execute Deployment</strong>: Trigger the second pipeline, which automatically pulls the pre-built Docker image and runs three parallel containers (Dev, Test, Prod_New) that handle authentication, copying, and rebinding processes</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758865553006/19a6a058-e7a0-48ae-84f1-48db3990462e.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The key advantage is that once such an image built for your requirement we can reuse and end users only need to focus on Phase 2 - the deployment execution of image/containers. This separation ensures that adhoc deployments are fast, reliable, and require minimal technical knowledge, while the complex image building process remains automated and invisible to operators.The parallel container execution significantly reduces deployment time, with each environment running independently using the same pre-built, tested image foundation.</div>
</div>

<p><strong>Once the blue path establishes the Docker image through the build pipeline and the green path deploys identical containers bound to their respective workspace environments, developers can seamlessly begin their modification workflow. Starting from the development environment, teams can follow familiar PowerBI deployment pipelines to systematically promote artifacts from one stage to another.</strong></p>
<p><strong>This solution elegantly integrates with existing PowerBI deployment architecture, maintaining consistency with established practices while providing a containerized foundation that ensures environment consistency and deployment reliability</strong></p>
<p>Following image illustrates the workspace artifact progression in Production: image (1) captures the baseline state prior to container deployment, with images (2), (3), and (4) showing the successive states following DevOps pipeline execution for the container releases.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758880204029/6f15017b-b9cd-4586-a770-53350644a035.png" alt class="image--center mx-auto" /></p>
<p>Finally, we can configure the <strong>PowerBI app</strong> setup as below by assigning custom report names and managing component visibility and swapping behavior as required, replicating the flexible configuration capabilities found in function app deployments.<strong>The point is is maintaining identical reports and models across environments, enabling developers to work on reports and models without disrupting user activity or causing downtime.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758880712647/6d99dc13-9b3c-4630-9a5c-e4b6a136e905.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-container-setup"><strong>Container Setup</strong></h2>
<blockquote>
<p>This section touches on Docker image creation and registry publishing concepts. While I'm walking through the entire process for comprehensive understanding, end users only need to focus on the deployment execution—the image creation is a one-time setup that's already handled.</p>
</blockquote>
<p>Here's the <strong>Dockerfile</strong> structure: The container includes Python 3.11 slim package as the base runtime, the Microsoft Fabric CLI for workspace operations, and the requests library for PowerBI REST API interactions. The entrypoint script handles authentication and delegates to the main deployment logic.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.11</span>-slim

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    ca-certificates curl git build-essential \
    &amp;&amp; rm -rf /var/lib/apt/lists/*</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Install Fabric CLI and required Python packages</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install --no-cache-dir --upgrade ms-fabric-cli requests</span>

<span class="hljs-comment"># Copy deployment scripts</span>
<span class="hljs-keyword">COPY</span><span class="bash"> deploy.py /app/deploy.py</span>
<span class="hljs-keyword">COPY</span><span class="bash"> entrypoint.sh /entrypoint.sh</span>
<span class="hljs-keyword">RUN</span><span class="bash"> chmod +x /entrypoint.sh</span>

<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [<span class="hljs-string">"/entrypoint.sh"</span>]</span>
</code></pre>
<p>For comprehensive technical documentation, including complete scripts and configuration files, refer to references section.</p>
<p>The ready-to-use Docker image is available on Docker Hub at <a target="_blank" href="https://hub.docker.com/repository/docker/nalakan/pbi_blue_green_dep_v1/general">https://hub.docker.com/repository/docker/nalakan/pbi_blue_green_dep_v1/general</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758859216966/9e8224b3-f6aa-4d21-b541-67330fccaaf9.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758866468875/6cc937a8-0737-48bc-bd6d-c96dc206be74.png" alt class="image--center mx-auto" /></p>
<p><strong>Build Pipeline</strong> <strong>(Image creation Process when there is a change to Prod source artifacts)</strong></p>
<p>The build pipeline triggers automatically when code changes are pushed to the main branch:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span>
  <span class="hljs-attr">branches:</span>
    <span class="hljs-attr">include:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-attr">include:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">Dockerfile</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">entrypoint.sh</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">deploy.py</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">azure-pipelines-build-image.yml</span>

<span class="hljs-attr">pool:</span>
  <span class="hljs-attr">vmImage:</span> <span class="hljs-string">'ubuntu-latest'</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">imageRepository</span>
    <span class="hljs-attr">value:</span> <span class="hljs-string">'nalakan/pbi_blue_green_dep_v1'</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">imageTag</span>
    <span class="hljs-attr">value:</span> <span class="hljs-string">'$(Build.BuildId)'</span>

<span class="hljs-attr">steps:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span> <span class="hljs-string">self</span>

<span class="hljs-comment"># Fix line ending issues for shell scripts</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
    echo "Normalizing .sh files to LF line endings..."
    find $(Build.SourcesDirectory) -type f -name "*.sh" -exec sed -i 's/\r$//' {} +
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Normalize</span> <span class="hljs-string">shell</span> <span class="hljs-string">scripts</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">Docker@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">image</span> <span class="hljs-string">to</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Hub</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">buildAndPush</span>
    <span class="hljs-attr">repository:</span> <span class="hljs-string">$(imageRepository)</span>
    <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile</span>
    <span class="hljs-attr">containerRegistry:</span> <span class="hljs-string">dockerhub-connection</span>
    <span class="hljs-attr">tags:</span> <span class="hljs-string">|
      $(imageTag)
      latest</span>
</code></pre>
<p><strong>Image Deployment Pipeline (Container Creation and Cross-Workspace Artifact Deployment)</strong></p>
<p>This pipeline is manually triggered and leverages Azure DevOps variable groups for configuration management,service principal authentication and creating containers and deploying artifacts across multiple workspaces as illustrated in the <a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1758860370586/eaeb350c-13cf-4c4d-a316-2703f721d327.gif">referenced image.</a></p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span> <span class="hljs-string">none</span>   <span class="hljs-comment"># Manual execution only</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">group:</span> <span class="hljs-string">Fabric-ServicePrincipal</span>    <span class="hljs-comment"># Service principal credentials</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">group:</span> <span class="hljs-string">Fabric-DeploymentSettings</span>  <span class="hljs-comment"># Workspace and item names</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">imageRepository</span>
    <span class="hljs-attr">value:</span> <span class="hljs-string">'nalakan/pbi_blue_green_dep_v1'</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">imageTag</span>
    <span class="hljs-attr">value:</span> <span class="hljs-string">'latest'</span>

<span class="hljs-attr">stages:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">stage:</span> <span class="hljs-string">Deploy</span>
  <span class="hljs-attr">jobs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job:</span> <span class="hljs-string">DeployToEnvs</span>
    <span class="hljs-attr">displayName:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">Dev</span> <span class="hljs-string">/</span> <span class="hljs-string">Test</span> <span class="hljs-string">/</span> <span class="hljs-string">Prod_New</span> <span class="hljs-string">via</span> <span class="hljs-string">Docker</span> <span class="hljs-string">image</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">docker</span> <span class="hljs-string">pull</span> <span class="hljs-string">$(imageRepository):$(imageTag)</span>
      <span class="hljs-attr">displayName:</span> <span class="hljs-string">Pull</span> <span class="hljs-string">image</span>

    <span class="hljs-comment"># Deploy to Development</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
        docker run --rm \
          -e FABRIC_CLIENT_ID=$(FabricClientId) \
          -e FABRIC_CLIENT_SECRET=$(FabricClientSecret) \
          -e FABRIC_TENANT_ID=$(FabricTenantId) \
          $(imageRepository):$(imageTag) \
            --source-workspace "$(ProdWorkspaceName)" \
            --target-workspace "$(DevWorkspaceName)" \
            --report-name "$(FabricReportName)" \
            --semantic-model-name "$(FabricModelName)" \
            --prefix Dev-
</span>      <span class="hljs-attr">displayName:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">Dev</span>
</code></pre>
<p>That's it ! The entire process(deploying the artifacts via containers) typically completes in under 10 minutes (These results are based on Azure DevOps free tier usage, so the numbers may not accurately represent enterprise-level performance benchmarks) deploying your parameterized reports across all three environments with zero manual intervention. Each environment gets properly prefixed items (Dev-, Test-, Prod_New_) that are fully functional and independently testable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758889198119/d54de7fb-6db9-4c00-8811-f76227ef900e.png" alt class="image--center mx-auto" /></p>
<p>The beauty of this approach is that once the initial setup is complete, deployments become a simple parameter change and button click. No complex commands, no manual copying, no risk of forgetting to rebind reports to datasets. Everything just works as a charm.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This PowerBI DevOps solution uses blue-green deployment patterns with Docker, Azure DevOps CI/CD, and Fabric CLI integration. It complements Microsoft's standard deployment pipelines by adding blue-green deployment functionality that isn't natively available in Power BI. The approach enables zero-downtime deployments and instant rollbacks across development, test, and production environments while maintaining data consistency.</p>
<h2 id="heading-acknowledgments">Acknowledgments 🙏</h2>
<p>Thanks to Leon and Dennes @ <a target="_blank" href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://onyxdata.co.uk/&amp;ved=2ahUKEwii0tfAnfaPAxXDTWwGHYoSN7MQFnoECB4QAQ&amp;usg=AOvVaw0rgeyMibqo1YwhMJFi-0y2">Onyx Data UK</a> for sparking the idea for the blue-green deployment for PowerBI. The implementation methodology and tools,techniques used shown here in this article is how I approached bringing their initial idea to life.</p>
<h2 id="heading-resources">Resources</h2>
<p><a target="_blank" href="https://github.com/nalakan/PowerBIMeetsDocker">https://github.com/nalakan/PowerBIMeetsDocker</a></p>
<p><a target="_blank" href="https://hub.docker.com/repository/docker/nalakan/pbi_blue_green_dep_v1/general">https://hub.docker.com/repository/docker/nalakan/pbi_blue_green_dep_v1/general</a></p>
<p><a target="_blank" href="https://microsoft.github.io/fabric-cli/">https://microsoft.github.io/fabric-cli/</a></p>
<p><strong>Thanks for reading</strong> 📖! Hope this post added something valuable to your knowledge and sparked new ideas to explore further</p>
]]></content:encoded></item><item><title><![CDATA[Implementing Retrieval-Augmented Generation (RAG) in Azure AI Foundry w/ Microsoft Fabric as a Data Source]]></title><description><![CDATA[The RAG tooling ecosystem has exploded-LangChain, LlamaIndex etc and dozens of specialized vector databases each solve pieces of the puzzle beautifully. But enterprise implementations aren't just about technical capabilities; they're about reducing c...]]></description><link>https://bidiaries.com/implementing-retrieval-augmented-generation-rag-in-azure-ai-foundry-w-microsoft-fabric-as-a-data-source</link><guid isPermaLink="true">https://bidiaries.com/implementing-retrieval-augmented-generation-rag-in-azure-ai-foundry-w-microsoft-fabric-as-a-data-source</guid><category><![CDATA[Azure]]></category><category><![CDATA[azure ai services]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[Azure AI Foundry]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[streamlit]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[gen ai]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 19 Aug 2025 10:15:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755594527964/e1414e94-4620-46aa-b495-df98aa762ecb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The RAG tooling ecosystem has exploded-LangChain, LlamaIndex etc and dozens of specialized vector databases each solve pieces of the puzzle beautifully. But enterprise implementations aren't just about technical capabilities; they're about reducing complexity, maintaining security boundaries, and scaling reliably. When your organization has already invested in the Microsoft stack, Azure AI Foundry with Fabric offers something beyond technical merit. It offers integration depth that external tools simply cannot match.</p>
<p><em>Here’s what we’re setting out to achieve in final phase…</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755595571288/a08daecc-029e-4d4c-924d-4e885914272a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-understanding-rag">Understanding RAG</h2>
<p>If you've been keeping an eye on AI trends, you've probably heard of <strong>RAG</strong> ; short for <strong>Retrieval-Augmented Generation</strong>. It's a method for giving large language models (LLMs) access to your own data, so they can answer questions with <strong>accurate, up-to-date, and context-rich information</strong> instead of relying only on what they were trained on.</p>
<h2 id="heading-how-rag-works">How RAG Works</h2>
<p>RAG creates a powerful synergy between your data and LLM capabilities. When a user submits a question, the system searches your data repository to find relevant information. The user question is then combined with matching results and sent to the LLM through a carefully crafted prompt. The LLM uses both the question and retrieved context to generate comprehensive, data-driven answers.</p>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview?tabs=docs"><img src="https://learn.microsoft.com/en-us/azure/ai-foundry/media/index-retrieve/rag-pattern.png" alt="Image Courtesy : Microsoft" /></a></p>
<h2 id="heading-the-core-components-embeddings-indexes-and-vector-databases">The Core Components: Embeddings, Indexes, and Vector Databases</h2>
<p><strong>Embeddings</strong> are numerical representations of text created by specialized AI models. These models convert words, sentences, or documents into high-dimensional vectors (arrays of numbers) that capture semantic meaning. Similar concepts produce similar vectors "car" and "automobile" become nearly identical number sequences despite being different words.</p>
<p><strong>Indexes</strong> serve as the foundation for efficient data retrieval. They're specialized data structures that enable fast, accurate searches across your information repository. RAG indexes combine multiple search methods:</p>
<ul>
<li><p><strong>Keyword searches</strong> for exact term matching</p>
</li>
<li><p><strong>Semantic searches</strong> for conceptual similarity</p>
</li>
<li><p><strong>Vector searches</strong> for nuanced content relationships</p>
</li>
<li><p><strong>Hybrid approaches</strong> that merge these capabilities</p>
</li>
</ul>
<p><strong>Vector Databases</strong> are storage systems optimized specifically for managing and searching embeddings. Unlike traditional databases that rely on exact matches, vector databases excel at similarity searches across millions of vectors in milliseconds.</p>
<h2 id="heading-typical-rag-workflow">Typical RAG Workflow</h2>
<ol>
<li><p><strong>Document Processing</strong>: Your documents are broken into chunks and converted into embeddings</p>
</li>
<li><p><strong>Storage</strong>: These embeddings are stored in a vector database (your searchable index)</p>
</li>
<li><p><strong>Query Processing</strong>: User questions are converted into embeddings using the same model</p>
</li>
<li><p><strong>Retrieval</strong>: The vector database finds the most <mark>semantically</mark> similar content</p>
</li>
<li><p><strong>Generation</strong>: Retrieved context plus the original question are sent to the LLM for accurate, contextual answers(Most often using a different model )</p>
</li>
</ol>
<p>This approach ensures responses are grounded in your actual data while leveraging the LLM's natural language capabilities.</p>
<p><strong>RAG patterns that include Azure AI Search</strong></p>
<p>Azure AI Search provides scalable search infrastructure that indexes diverse content types and enables retrieval through APIs, applications, and AI agents. The platform offers native integrations with Azure's AI ecosystem including OpenAI services, AI Foundry, and Machine Learning while supporting extensible architectures for third-party and open-source model integration.</p>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview?tabs=docs"><img src="https://learn.microsoft.com/en-us/azure/search/media/retrieval-augmented-generation-overview/architecture-diagram.png" alt="Architecture diagram of information retrieval with search and ChatGPT." /></a></p>
<p><img src="https://microsoft.github.io/aitour-build-a-copilot-on-azure-ai/img/rag-design-pattern.png" alt="RAG" /></p>
<p>Let’s dive into practical implementation from here onwards.This would include both UI based approach and code first approach.</p>
<blockquote>
<p>Note : I have omitted minor or commonly known details, focusing instead on explaining and elaborating the RAG pattern and it’s core setup and flow.</p>
</blockquote>
<h2 id="heading-approach-1-low-no-code-solution">Approach 1 : Low / No-Code Solution</h2>
<ol>
<li><p>Create an Azure OpenAI resource<br /> The initial step is to create an OpenAI resource in Azure, as illustrated below.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755518102864/58061e13-29c0-46bd-b5ac-9772dd27b4c6.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Navigate to the Azure AI Foundry portal and create an <strong>embedding</strong> deployment. You may select any base model of your choice; in this example, the ‘<em>text-ada’</em> model has been selected.(text-embedding-ada-002 is part of gpt-3 model family.)</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755497554056/fdb59f9e-d9d1-43ea-b4a6-b8d249b772d0.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Jump back to Azure portal and create an <strong>Azure AI Search</strong> Resource (When allocating AI search mindful of the cost of the resource utilization)</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755532489587/35baba0e-2efd-4c17-b50a-28190622e8ee.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Connect the document folder within the Fabric ‘Lakehouse’ files area and vectorize the data.(Once we navigate to the created AI search resource click as below.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755498288243/f1faab74-185b-487d-beea-652a55c8f4c2.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Select Fabric OneLake files as source for the vectorization.Since the document I am using is purely text based document ( I used <a target="_blank" href="https://adoption.microsoft.com/files/copilot-studio/Agent-governance-whitepaper.pdf"><strong>Microsoft Whitepaper on AI governance</strong></a> as my source file which consists 32 pages) and this was uploaded to Fabric Lakehouse files section by creating a folder</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755597751964/f6ad83de-d645-4078-ac89-f8330b99be94.png" alt class="image--center mx-auto" /></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755498327964/34ed8285-3959-4921-a222-079228c23539.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Select <strong>‘RAG’</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755498405914/1130125f-3b5b-4370-9046-ab0e39e3163a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Next, specify the Fabric ‘Lakehouse’ URL along with the folder path where the files are located.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755518501502/18b4ca94-8347-4aa8-a5de-98f4765abaa1.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>After completing these steps, navigate to the <strong>‘<em>Indexes’</em></strong> section in the <strong>Azure AI Search</strong> portal. You will notice that an index is created within a short period of time.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755498707517/1a0dd6b1-77d0-4440-8f33-39e37d762ffe.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Now we can create a new <strong>agent</strong> in Azure AI foundry and attach the <strong>created Azure AI search</strong> resource as a knowledge tool and play with the data in <strong>agent playground.</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755499887646/73dab9dc-2bc2-49ef-8890-9967a64e1f24.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p><strong>For the search type I have selected Hybrid search (Vector+Keyword)</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755596100309/1934494c-34d5-4829-add5-21f551e1717d.png" alt class="image--center mx-auto" /></p>
<p><strong>Finally Try in agent playground 🏐</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755596040250/a31a2d4c-79bc-4cfe-aac5-b452f655a80d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-showcase-01-with-agents-playground-ui">Showcase <strong>- [ 01 ] with Agents playground UI</strong></h2>
<p>It can be observed that the reference has been applied correctly, ensuring that no external sources were used as knowledge.(<strong>“AI Governance whitepaper.pdf”</strong>)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755500187101/046599c2-8527-4dd1-a384-13c8ef387dae.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Tool calling succeeded, as confirmed within the thread status.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755595928845/a892c670-22b4-4d80-b009-005b715d9c47.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Up to this point, we have explored how to create a RAG pattern using a low-code/no-code solution. We will now proceed with the code-first approach, which offers more flexibility and customization options for developers who want fine-grained control over their RAG implementation.</div>
</div>

<hr />
<h2 id="heading-approach-2-code-first-implementation">Approach 2: Code-First Implementation</h2>
<h3 id="heading-azure-ai-foundry-sdk-ai-projects-client-library">Azure AI Foundry SDK / AI Projects client library</h3>
<p>We'll primarily use the <mark>AI Projects client library </mark> is part of the <mark>Azure AI Foundry SDK</mark>, and provides easy access to resources in your Azure AI Foundry Project. ( The <strong>'</strong><a target="_blank" href="http://azure.ai"><strong>azure.ai</strong></a><strong>.agents.models'</strong> package is a sub-module under this client library that handles agent-related operations.)</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Following Microsoft’s Build announcement in April 2025, numerous enhancements were introduced to the <strong>Azure AI Foundry “Projects” library.</strong> If you're using earlier versions, it's highly recommended to upgrade to the latest library . Also note I am using new foundry based project path and not hub based project (legacy) path.</div>
</div>

<p><strong>Install the libraries</strong> - (Note that the dependent package <a target="_blank" href="https://pypi.org/project/azure-ai-agents/">azure-ai-agents</a> <a target="_blank" href="https://pypi.org/project/azure-ai-agents/">will be install</a> as a result, if not already installed, to support <code>.agent</code> operations on the client.)</p>
<pre><code class="lang-python">!pip install --upgrade azure-ai-projects azure-identity load_dotenv
!python -m pip install --upgrade pip
</code></pre>
<p>This code loads configuration values (like client ID, secret, tenant ID, and endpoint) from an external <code>.env</code> file (<code>api_settings.env</code>) into Python variables.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv
load_dotenv(<span class="hljs-string">'api_settings.env'</span>)
CLIENT_ID = os.getenv(<span class="hljs-string">"CLIENT_ID"</span>)
CLIENT_SECRET = os.getenv(<span class="hljs-string">"CLIENT_SECRET"</span>)
TENANT_ID = os.getenv(<span class="hljs-string">"TENANT_ID"</span>)
PROJECT_ENDPOINT = os.getenv(<span class="hljs-string">"PROJECT_ENDPOINT"</span>)
</code></pre>
<p>The script below demonstrates the following steps.</p>
<ul>
<li><strong>Authentication and Setup</strong> – The script retrieves Azure credentials (Tenant ID, Client ID, Client Secret) from environment variables and authenticates using <code>ClientSecretCredential</code>. It then initializes the <code>AIProjectClient</code> with the given project endpoint.</li>
</ul>
<blockquote>
<p>When using the Azure AI Projects library with Entra ID authentication, we must authenticate via a service principal or app registration. To enable this, assign the <strong>Azure AI User</strong> role (built-in Azure RBAC role) on the Azure AI Foundry resource to your identity. This ensures your service principal has permission to access AI projects using Microsoft Entra ID.</p>
</blockquote>
<ul>
<li><p><strong>Connection Discovery</strong> – It queries the list of configured project connections and identifies the <strong>Azure AI Search</strong> resource by inspecting connection metadata, extracting the corresponding <code>connection_id</code>.</p>
</li>
<li><p><strong>Tool Initialization</strong> – An <code>AzureAISearchTool</code> instance is created using the connection ID and a predefined index name, enabling integration of search capabilities into the agent.</p>
</li>
<li><p><strong>Agent Creation</strong> – A new agent is provisioned with the GPT-4.1-mini model, custom instructions for Retrieval-Augmented Generation (RAG) queries, and the AI Search tool attached as a resource.</p>
</li>
<li><p><strong>Thread Creation</strong> – A dedicated conversation thread is established to maintain the dialogue state with the agent.</p>
</li>
<li><p><strong>Interactive Execution Loop</strong> –</p>
<ul>
<li><p>User inputs are continuously read from the console.</p>
</li>
<li><p>Inputs are posted as messages to the agent.</p>
</li>
<li><p>The agent is executed (<code>create_and_process</code>), responses are retrieved, and results are displayed.</p>
</li>
<li><p>The loop terminates when the user enters <code>"end"</code>, after which the agent is deleted for cleanup.</p>
</li>
</ul>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> azure.ai.projects <span class="hljs-keyword">import</span> AIProjectClient
<span class="hljs-keyword">from</span> azure.identity <span class="hljs-keyword">import</span> ClientSecretCredential
<span class="hljs-keyword">from</span> azure.ai.agents.models <span class="hljs-keyword">import</span> AzureAISearchTool

<span class="hljs-comment"># Entry point for the script</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    <span class="hljs-comment"># Load Azure credentials from environment variables</span>
    credential = ClientSecretCredential(
        tenant_id=os.getenv(<span class="hljs-string">"TENANT_ID"</span>),
        client_id=os.getenv(<span class="hljs-string">"CLIENT_ID"</span>),
        client_secret=os.getenv(<span class="hljs-string">"CLIENT_SECRET"</span>)
    )

    <span class="hljs-comment"># Create a client instance for interacting with the Azure AI Project</span>
    project_client = AIProjectClient(
        credential=credential,
        endpoint=os.environ[<span class="hljs-string">"PROJECT_ENDPOINT"</span>]  <span class="hljs-comment"># Project endpoint reference</span>
    )

    <span class="hljs-comment"># Retrieve available project connections to locate the AI Search resource</span>
    conn_list = project_client.connections.list()
    conn_id = <span class="hljs-string">""</span>

    <span class="hljs-comment"># Identify the Azure AI Search connection by checking metadata fields</span>
    <span class="hljs-comment"># Assumes only one AI Search connection is configured in the project</span>
    <span class="hljs-keyword">for</span> conn <span class="hljs-keyword">in</span> conn_list:
        properties = conn.get(<span class="hljs-string">"properties"</span>, {})
        metadata = properties.get(<span class="hljs-string">"metadata"</span>, {})
        <span class="hljs-keyword">if</span> metadata.get(<span class="hljs-string">"type"</span>, <span class="hljs-string">""</span>).upper() == <span class="hljs-string">"AZURE AI SEARCH"</span>:
            conn_id = conn[<span class="hljs-string">"id"</span>]
            <span class="hljs-keyword">break</span>

    print(conn_id)

    <span class="hljs-comment"># Reassign the valid connection ID for AI Search</span>
    conn_id = conn[<span class="hljs-string">"id"</span>]

    <span class="hljs-comment"># Configure the Azure AI Search tool with the discovered connection and index name</span>
    ai_search = AzureAISearchTool(index_connection_id=conn_id, index_name=<span class="hljs-string">"rag-1754502262882"</span>)

    <span class="hljs-comment"># Provision an AI Agent and attach the AI Search tool for RAG-style queries</span>
    agent = project_client.agents.create_agent(
        model=<span class="hljs-string">"gpt-4.1-mini"</span>,
        name=<span class="hljs-string">"my-agent-aisearch"</span>,
        instructions=<span class="hljs-string">"You are a helpful agent to perform RAG Queries using AI Search"</span>,
        tools=ai_search.definitions,
        tool_resources=ai_search.resources,
    )
    print(<span class="hljs-string">f"Created agent, ID: <span class="hljs-subst">{agent.id}</span>"</span>)

    <span class="hljs-comment"># Initialize a new conversation thread for maintaining dialog state</span>
    thread = project_client.agents.threads.create()
    print(<span class="hljs-string">f"Created thread, ID: <span class="hljs-subst">{thread.id}</span>"</span>)

    <span class="hljs-comment"># Continuous user interaction loop</span>
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        user_input = input(<span class="hljs-string">"User: "</span>)
        <span class="hljs-keyword">if</span> user_input.lower() == <span class="hljs-string">"end"</span>:
            project_client.agents.delete_agent(agent.id)
            print(<span class="hljs-string">"Ending the conversation."</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Post user input as a message to the agent</span>
        message = project_client.agents.messages.create(
            thread_id=thread.id,
            role=<span class="hljs-string">"user"</span>,
            content=user_input,
        )

        <span class="hljs-comment"># Run the agent and process the response</span>
        run = project_client.agents.runs.create_and_process(thread_id=thread.id, agent_id=agent.id)
        print(<span class="hljs-string">f"Run finished with status: <span class="hljs-subst">{run.status}</span>"</span>)

        <span class="hljs-keyword">if</span> run.status == <span class="hljs-string">"failed"</span>:
            print(<span class="hljs-string">f"Run failed: <span class="hljs-subst">{run.last_error}</span>"</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Fetch and print the agent’s most recent reply</span>
        messages = project_client.agents.messages.list(thread_id=thread.id)
        <span class="hljs-keyword">for</span> agent_response <span class="hljs-keyword">in</span> messages:
            <span class="hljs-keyword">if</span> agent_response.text_messages:
                last_text = agent_response.text_messages[<span class="hljs-number">-1</span>]
                print(<span class="hljs-string">f"<span class="hljs-subst">{agent_response.role}</span>: <span class="hljs-subst">{last_text.text.value}</span>"</span>)   

    <span class="hljs-comment"># Final cleanup of the agent instance</span>
    project_client.agents.delete_agent(agent.id)
    print(<span class="hljs-string">"Conversation ended"</span>)
</code></pre>
<h2 id="heading-showcase-02-with-visual-studio-code-in-locally">Showcase <strong>- [ 02 ] with Visual Studio code in locally</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755543484412/95b72106-55ad-456c-a7f9-9deb638678a3.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-showcase-03-using-streamlit-frontend">Showcase <strong>- [ 03 ] using Streamlit Frontend</strong></h2>
<blockquote>
<p><strong>Streamlit</strong> is an open-source Python framework that makes it easy to build interactive web apps for data and machine learning projects.</p>
</blockquote>
<p>📤I deployed my project on <strong>Streamlit Community Cloud</strong> and adapted the code to work seamlessly with the <strong>Streamlit</strong> frontend. This enables me to access the app from anywhere through a customized, user-friendly interface.</p>
<p>By the way, what key information are you looking to extract from that <a target="_blank" href="https://adoption.microsoft.com/files/copilot-studio/Agent-governance-whitepaper.pdf">Microsoft whitepaper?</a> 📄🤔</p>
<p>I’ve made it public 🌍, so feel free to give it a try 🚀.</p>
<p><strong>[ Sample Q’s you can try out for:</strong></p>
<ul>
<li><p>What specific governance controls do we need to implement for each type of agent creator (End Users, Makers, and Developers) in our organization?</p>
</li>
<li><p>How can we effectively use Microsoft Purview's Data Loss Prevention policies to prevent our agents from accessing highly confidential SharePoint content?</p>
</li>
<li><p>What's the best approach for setting up Power Platform environments and pipelines to ensure secure agent development and deployment across our teams? etc ]</p>
</li>
</ul>
<blockquote>
<p>Link : <a target="_blank" href="https://ragaifoundry.streamlit.app/">https://ragaifoundry.streamlit.app/</a><br />If you’re unable to access the working site, it’s probably because my Azure free credits ran out 💳⚡. You can still find all my project assets in the References section below 📂</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755580601209/6821edc3-1fbc-4dd3-89d2-4c7cfcf4becd.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>Building enterprise RAG solutions with Azure AI Foundry reveals several crucial insights: prioritize data quality over complexity, leverage hybrid search for optimal results, and choose between low-code UI approaches for rapid prototyping or code-first methods for customization. Most importantly, start small and scale gradually while continuously monitoring performance.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Azure AI Foundry transforms enterprise RAG implementation from complex to accessible. Whether you prefer UI-based configuration, programmatic development, or web deployment, the platform provides enterprise-grade security and scalability without sacrificing ease of use.</p>
<p>The three approaches demonstrated <strong><em>Agent playground, Azure AI Foundry SDK, and Streamlit web app</em></strong> showcase the platform's flexibility for different team needs and technical requirements. As organizations increasingly need AI solutions that integrate seamlessly with existing Microsoft infrastructure, Azure AI Foundry offers both immediate value and long-term strategic advantages.</p>
<hr />
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview?tabs=docs">Azure AI Search - Retrieval Augmented Generation Overview</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/retrieval-augmented-generation">Azure AI Foundry - RAG Concepts</a></p>
</li>
<li><p><a target="_blank" href="https://adoption.microsoft.com/files/copilot-studio/Agent-governance-whitepaper.pdf">Microsoft AI Agents Governance Whitepaper</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/nalakan/AI_Foundry_rag_doc_search?tab=readme-ov-file#rag-based-intelligent-document-search-using-azure-ai-foundrysearch">Project Assets</a></p>
</li>
</ul>
<hr />
<p><em>Thanks for reading! If you found this guide helpful, consider sharing it with others who might benefit from implementing RAG solutions in their organizations with Microsoft AI Foundry.</em></p>
]]></content:encoded></item><item><title><![CDATA[Access your 𝗠𝗶𝗰𝗿𝗼𝘀𝗼𝗳𝘁 𝗙𝗮𝗯𝗿𝗶𝗰 Data with external applications using "𝗚𝗿𝗮𝗽𝗵𝗤𝗟" 𝗽𝗹𝗮𝘆𝗴𝗿𝗼𝘂𝗻𝗱]]></title><description><![CDATA[Picture this : you've got valuable data locked away in Microsoft Fabric, perfectly organized and secure. But now you need to share that data with external applications, power dynamic dashboards, or integrate with third-party tools. The question that ...]]></description><link>https://bidiaries.com/getting-your-data-out-of-microsoft-fabric-with-graphql</link><guid isPermaLink="true">https://bidiaries.com/getting-your-data-out-of-microsoft-fabric-with-graphql</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[graphql api]]></category><category><![CDATA[fabric]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[dataengineering]]></category><category><![CDATA[lakehouse]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[#microsoft-azure]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Sat, 26 Jul 2025 19:48:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/9WWQWYmHBCk/upload/21664edfecd365b95ad245124279c082.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Picture this : you've got valuable data locked away in Microsoft Fabric, perfectly organized and secure. But now you need to share that data with external applications, power dynamic dashboards, or integrate with third-party tools. The question that keeps every data engineer up at night is: how do you provide secure, efficient external access to only authorized parties?</p>
<p>If you've been wrestling with this challenge, Microsoft Fabric's <strong>GraphQL API</strong> might just be the solution you've been looking for.</p>
<h3 id="heading-why-graphql-changes-the-game">Why GraphQL Changes the Game</h3>
<p>Think of traditional REST APIs as that over eager friend who tells you their entire life story when you just asked how their day went. You get everything-all the fields, all the data whether you need it or not. Then you're stuck filtering through the noise to find what you actually wanted.</p>
<p>GraphQL, on the other hand, is like having a conversation with someone who listens. You ask for exactly what you need, and that's precisely what you get. Need just customer names and sales amounts? That's all you receive. Want to combine data from multiple related tables? One elegant query handles it all.</p>
<p>Here's what makes GraphQL particularly powerful for Fabric:</p>
<ul>
<li><p><strong>Precision</strong>: Request only the columns you need, nothing more</p>
</li>
<li><p><strong>Efficiency</strong>: Combine multiple data requests into a single call</p>
</li>
<li><p><strong>Flexibility</strong>: Query related data in nested structures that mirror real relationships</p>
</li>
<li><p><strong>Performance</strong>: Reduce network overhead and eliminate over-fetching</p>
</li>
</ul>
<h3 id="heading-data-path-for-graphql">Data Path for GraphQL</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753197004554/0ae4e791-3c12-462a-8f8c-553ae2302848.png" alt class="image--center mx-auto" /></p>
<p>A GraphQL API operates as a server that sits a top an HTTP server, making an API available for code interaction.</p>
<ul>
<li><p><strong>Centralized Data Access:</strong> The GraphQL server contains code capable of communicating with various data sources.</p>
</li>
<li><p><strong>JSON-Based Communication:</strong> Send a JSON document to the GraphQL server, containing security tokens and data requests. The server processes this, retrieves data from the underlying sources, and returns another JSON document containing the requested data, messages, and error codes all at once.</p>
</li>
<li><p><strong>Schema Definition:</strong> GraphQL employs a schema, very similar to a SQL schema with the exception it's over all kinds of data,which defines the structure of available fields and data types, ensuring consistent data access across disparate sources.</p>
</li>
<li><p><strong>Operations:</strong> GraphQL utilizes specific operation types:</p>
<ul>
<li><p><strong>Queries:</strong> For retrieving data, akin to SQL SELECT statements.</p>
</li>
<li><p><strong>Mutations:</strong> For changing data (insert, update, delete).</p>
</li>
<li><p><strong>Subscriptions:</strong> To "subscribe to data as it changes," enabling real-time updates.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-query-the-data-with-api-for-graphql"><strong>Query the data with API for GraphQL</strong></h3>
<blockquote>
<p>First we will focus on creating GraphQL resource and GraphQL endpoint inside Microsoft Fabric.</p>
</blockquote>
<ul>
<li><p>Go to the Microsoft Fabric workspace and select <strong>“New”</strong>, then choose <strong>“GraphQL API”</strong> to open the creation wizard.Enter a name and description for the <strong>GraphQL</strong>.</p>
</li>
<li><p>Click <strong>“Add a data source”</strong>, choose <strong>Warehouse</strong> as the source type, and select the target warehouse from the list.Click <strong>Connect</strong> to proceed to the screen for selecting tables .</p>
</li>
<li><p>Select the required tables or views and click <strong>Load</strong> to include them in the GraphQL API.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753201160210/11c85d52-34fe-4b8d-ad0e-64a6e2f81040.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I used the classic ‘Northwind dataset’ as my source to load into Warehouse as tables.In the left schema explorer pane shows the tables loaded.Before ran a query a relationship was built between ‘fact_sale’ and ‘dimension_customer’ table.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753552189012/5e03e7c3-cb73-44c9-98ac-910d55693726.png" alt class="image--center mx-auto" /></p>
<p>Then GraphQL query was created which <strong>fetches the first 10 sales records</strong> from the <code>fact_sales</code> table and <strong>retrieves customer details</strong> by traversing the relationship to the <code>dimension_customer</code> table.(This is a sample query to display the capability of nested structure of ‘json’ like format in GraphQL syntax.Fabric supports auto complete when coding inside the query pane which makes life much easy for the user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753552575877/73380cc2-dcee-4b6f-a1d0-fb718c395e0b.png" alt class="image--center mx-auto" /></p>
<p>In the results section it retrieves 10 sales transactions,and for each transaction, include related customer information in nested format.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753551963195/82e95c43-b046-43da-b562-f37f7bd99d0f.png" alt class="image--center mx-auto" /></p>
<p>So far so good.Now, let’s assume we need to retrieve the above results externally from an application. How can we achieve that? Let’s start the journey.</p>
<blockquote>
<p>The easiest starting point is to capture the code generated through the toolbar.As of today, the generated code comes in two flavours. In this demo I am using ‘Python’ as my code flavour. Set the code aside for now; we'll return to it shortly.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753204510399/8a93b2c0-7b82-4d43-851d-4d00958b566d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753204551188/e93cfa0a-2b89-42af-bee3-08b0471d652c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-bringing-it-to-external-applications">Bringing It to External Applications</h3>
<p>Now comes the interesting part: accessing this data from external applications. This is where many implementations fall short, but with proper setup, it becomes surprisingly straightforward.</p>
<p>An ‘App registration’ is needed first.I would not into details of creating ‘App registration’ as it is more straight forward.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753273464924/2a18fd2f-8fb2-4bee-b721-bc10070b94d4.png" alt class="image--center mx-auto" /></p>
<p>We need give required permissions to ‘App Registration’ as below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753273673446/3f62a8b0-f7c3-482b-94dd-9cb49348c049.png" alt class="image--center mx-auto" /></p>
<p>Then under Microsoft API’s select ‘PowerBI service’ and select as follows.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753273817605/f8d381fe-ec2a-45fd-ac0c-964190d81b45.png" alt class="image--center mx-auto" /></p>
<p>Then In the <strong><em>Manage &gt; Authentication</em></strong> section, click on <strong>Redirect URI configuration</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753374252825/add3038d-ed37-4854-a672-cfbdc98e2e49.png" alt class="image--center mx-auto" /></p>
<p>Here in the below ‘Redirect URI’ first I tested this in localhost:5500 and later I hosted the project in the link ‘<a target="_blank" href="https://nalakan.github.io/-fabric-graphql-spa/">https://nalakan.github.io/ms-fabric-graphql-spa/</a>’ with additional components.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753374278162/375f22ff-da2d-4e57-b398-4d20e95fdeae.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Base code which I copied from GraphQL ‘Generated code’ displays below.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753551117467/d6b3f09a-becf-415e-86c8-ed6679a00c73.png" alt class="image--center mx-auto" /></p>
<p>Here, you can observe that the <strong>GraphQL endpoint</strong> on line 23, along with the <strong>query(Line 24 -46)</strong> used in the Fabric interface, is being passed here.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can see that the terminal output matches exactly with the previous instance when we executed it inside Fabric, even when running the same code locally in VS Code.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753556368205/79f14bf9-cb6b-4d49-8409-a4e672babef6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-project-graphql-playground">🚀 <strong>Project : GraphQL Playground</strong></h2>
<p>Rather than stopping at basic functionality, I decided to build something more comprehensive. The result is a full-featured GraphQL playground that extends the Fabric experience for external users.</p>
<h2 id="heading-key-features"><strong>Key Features</strong></h2>
<ul>
<li><p><strong>Enhanced UI Integration</strong>: The interface feels familiar to anyone who's used the Fabric GraphQL explorer, but with additional functionality tailored for external access.</p>
</li>
<li><p><strong>Complete Schema Exploration</strong>: Users can browse the full schema and understand table relationships without needing access to the Fabric workspace itself.</p>
</li>
<li><p><strong>Direct Data Export</strong>: One of the most requested features-the ability to download query results directly to local files in various formats.</p>
</li>
<li><p><strong>Interactive Query Building</strong>: The playground provides real-time query validation and auto-completion, making it accessible to users with varying GraphQL experience levels.</p>
</li>
<li><p><strong>Comprehensive Documentation</strong>: Built-in docs and schema visualization help users understand data structures and build effective queries.</p>
</li>
</ul>
<h2 id="heading-application-components">Application Components</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753558861566/e3b14373-c111-40b1-ba13-e1ab6f8336e1.png" alt /></p>
<p>The application structure follows clean architecture principles, with the ‘App registration’ details from our earlier setup included in the <code>app.js</code> file. This security model leverages the ‘App Registration’ we created earlier, ensuring that only authorized users can access your Fabric data while maintaining proper security standards.</p>
<p>The project is fully functional and available at: <a target="_blank" href="https://nalakan.github.io/ms-fabric-graphql-spa/">https://nalakan.github.io/ms-fabric-graphql-spa/</a></p>
<p>The complete source code is available on GitHub: <a target="_blank" href="https://nalakan.github.io/ms-fabric-graphql-spa/">https://github.com/nalakan/ms-fabric-graphql-spa</a></p>
<p>Here’s a quick sneak peek 👀.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753375211052/f3611327-7c84-475f-977f-f46e15faa270.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753561267669/2b0d88b7-5cba-4d6b-81d9-f7e9363e0559.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Click on the image/gif for zoom</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753550416367/90968611-e3e4-4010-a021-7dfb7e7844ef.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-looking-forward">Looking Forward</h2>
<p>GraphQL's integration with Microsoft Fabric opens up exciting possibilities for data access patterns. As organizations increasingly need to share data across diverse applications and teams, having a flexible, secure, and performant API layer becomes crucial.</p>
<p>The playground project I've shared represents just the beginning. The same patterns can be extended to build custom dashboards, integrate with external analytics tools, or power mobile applications—all while maintaining the security and governance that enterprise data requires.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<p>For extra details about Fabric's GraphQL, I recommend checking out this nice guide:</p>
<p><a target="_blank" href="https://data-mozart.com/microsoft-fabric-api-for-graphql-everything-you-need-to-know/">https://data-mozart.com/microsoft-fabric-api-for-graphql-everything-you-need-to-know/</a></p>
<hr />
<p><em>Thanks for reading ! I'm always exploring new ways to work with Microsoft Fabric and AI .Stay tuned for more technical walkthroughs and practical implementations.</em></p>
]]></content:encoded></item><item><title><![CDATA[Power BI Slicer Defaults : 
Show Actual Values Dynamically -
(Not Just 'Current Month / Period')]]></title><description><![CDATA[Have you encountered a situation where a Power BI report seems stuck in time, with slicers for eg: month failing to reflect the latest data? It’s a common frustration - users continue to see outdated selections, even though newer entries have arrived...]]></description><link>https://bidiaries.com/how-to-set-default-slicer-selection-to-current-month-in-powerbi</link><guid isPermaLink="true">https://bidiaries.com/how-to-set-default-slicer-selection-to-current-month-in-powerbi</guid><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[PowerPlatform]]></category><category><![CDATA[visualization]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[tableau]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[data-modeling]]></category><category><![CDATA[dax]]></category><category><![CDATA[#data visualisation]]></category><category><![CDATA[power BI solutions]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Wed, 18 Jun 2025 08:14:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/FoKO4DpXamQ/upload/936998749c7fc051981dc201f0e8e514.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you encountered a situation where a Power BI report seems stuck in time, with slicers for eg: month failing to reflect the latest data? It’s a common frustration - users continue to see outdated selections, even though newer entries have arrived.</p>
<p>Say you shared a report in <strong>May 2025</strong>, with slicers set to <code>Year = 2025</code> and <code>Month = May</code>. Fast-forward to July or August, and unless someone manually updates the report or changes the default slicer values, it’ll still load with May selected. The report appears "stuck in time.".</p>
<p>This article introduces a clean, automated way to solve that. Using a PowerBI <strong>Button Slicer</strong> and a bit of simpler DAX, we’ll dynamically highlight the latest available month -without requiring manual updates or breaking best practices.</p>
<h2 id="heading-data-model">Data Model</h2>
<p>Here's a simplified look at the data model I'm using. Not every table is relevant for this walk-through, but the focus will be on the <code>Calendar</code> table (highlighted below in my model) and optionally Customer,Orders tables.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750223540800/80b759d3-36f4-449e-afd4-b58420c1fe15.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-1-create-a-calculated-column-latest-month-ph">Step 1: Create a Calculated Column -<code>Latest Month PH</code></h2>
<p>We start by adding a calculated column to the <code>Calendar</code> table called <code>Latest Month PH</code>. This column helps us identify the latest month dynamically and assign it a numeric value of <code>100</code>.(Assign a preferred number greater than 12 ,which is the total number of months) . Other months will keep their actual month number.</p>
<pre><code class="lang-markdown">Latest Month PH = 
VAR <span class="hljs-emphasis">_maxym= CALCULATE(MAX('Calendar'[YearMonth]),all())
RETURN
IF('Calendar'[YearMonth]=_</span>maxym,100, 'Calendar'[Month Number])
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750225320577/e91c3eb5-58bb-4cfa-82f7-4dfadf653dec.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-create-a-second-calculated-column-latest-month">Step 2: Create a Second Calculated Column -<code>Latest Month</code></h2>
<p>Next, let’s create another calculated column(‘Latest Month’ Column) to show the month name. This will help us display a more user-friendly label in the visual.</p>
<pre><code class="lang-markdown">Latest Month = 
VAR <span class="hljs-emphasis">_maxym= CALCULATE(MAX('Calendar'[YearMonth]),all())
RETURN
IF(
    'Calendar'[YearMonth]=_</span>maxym,'Calendar'[Month Name] &amp; "", 'Calendar'[Month Name])
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750225448314/8ffbf578-c8b8-403c-b307-9f2e18ca14c4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-3-setting-up-the-button-slicer">Step 3: Setting Up the ‘Button Slicer’</h2>
<ol>
<li><strong>Add a Button Slicer</strong> to the canvas.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750225874627/b9c06c6c-04ac-4ea3-a2b8-8b8d08a07020.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li><strong>Set the field</strong> to <code>Latest Month PH</code>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750226172258/e9046948-309b-4cff-b6ce-9c8b2b5086ed.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Under ‘<strong>Callout values’</strong>, toggle on the <strong>"Values"</strong> option / Turn off <strong>Values</strong>. The Magic will happen after this.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750226339233/f98ccdfa-6158-4732-8e72-3acc32ce4b56.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li>Still within ‘Callout values’, enable the <strong>"Label"</strong> and drag in the <code>Latest Month</code> column.Make sure you apply this setting to both the <strong>Default</strong> and <strong>Selected</strong> states if it doesn’t display in <strong>‘Selected’</strong> state.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750226590786/9649e26b-ca32-4ba9-b35e-a16170562f1d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-4-testing-it-in-action">Step 4 : Testing it in action</h2>
<p>To test this setup without waiting for a new calendar month to roll around, I created a pseudo column using <code>Month Number</code>&amp; propagated using parameter This lets me simulate a new month and verify that the button updates accordingly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750231004674/b8b07b86-4721-49c5-b15c-bbfcb1f5d138.png" alt class="image--center mx-auto" /></p>
<p>Lets change Month to Latest Month using a Parameter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750231064340/c56a53e2-78f4-4399-a3d7-8cc03d954401.png" alt class="image--center mx-auto" /></p>
<p>At the time of writing, the latest month is <strong>June</strong>, and sure enough, the slicer now shows “June” as expected -automatically and without manual input.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750231148134/904f3b47-d7e8-4adc-b272-0f2e0239116d.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750231491840/174d4948-b2bf-4e1f-bfbd-1fa090bed12d.gif" alt class="image--center mx-auto" /></p>
</blockquote>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Previously, I used workarounds like placeholders such as "Current Month" or "Latest Month" to display the most recent month using DAX measures. While these approaches were functional, they often fell short of user expectations - especially when users preferred seeing the actual month name instead of a numeric value or generic label.</div>
</div>

<blockquote>
<p>With this new approach, we achieve a dynamic and user-friendly experience by combining the flexibility of the button slicer with two lightweight calculated columns in the ‘Calendar’ table. Importantly, these additions do not significantly impact the model size, allowing us to maintain performance and follow best practices without unnecessarily bloating in the ‘Calendar’ dimension or Facts.</p>
</blockquote>
<p>Feel free to share how you have tackled similar scenarios in the comments below</p>
<p><strong>Thank You for Reading !!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Real-Time Intelligence in Action : Tracking Prime Time News Channel's YouTube Live Streams with Microsoft Fabric RTI]]></title><description><![CDATA[A month ago, I promised on my previous LinkedIn post that I'd show you how to track YouTube live streams from Sri Lanka's prime-time news channels using the Real-Time Intelligence (RTI) capabilities in Microsoft Fabric — and here we are. This blog ta...]]></description><link>https://bidiaries.com/real-time-intelligence-in-action-tracking-prime-time-news-channels-youtube-live-streams-with-microsoft-fabric-rti</link><guid isPermaLink="true">https://bidiaries.com/real-time-intelligence-in-action-tracking-prime-time-news-channels-youtube-live-streams-with-microsoft-fabric-rti</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[real time analytics]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[news]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[youtube]]></category><category><![CDATA[YouTube Channel]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[KQL]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Thu, 22 May 2025 17:08:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747851992461/b68bb7b6-dacf-44d2-9142-0084a1926519.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A month ago, I promised on my previous LinkedIn <a target="_blank" href="https://www.linkedin.com/posts/nalakan_realtimeintelligence-microsoftfabric-youtubeanalytics-activity-7309993465868623873-CwWX?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAyVExwBubhXZKdXDzfpfHPG0HiZx4gbYKQ">post</a> that I'd show you how to track <strong>YouTube live streams from Sri Lanka's prime-time news channels</strong> using the <strong>Real-Time Intelligence (RTI)</strong> capabilities in <strong>Microsoft Fabric</strong> — and here we are. This blog takes you through how I built a streaming data solution that tracks live viewer metrics in real time using Fabric, YouTube APIs, and PySpark,Python Notebook.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Disclaimer : This is a technical exploration for learning purposes. I have no affiliation with the featured TV channels,YouTube and this post doesn't serve any promotional intent.</div>
</div>

<h2 id="heading-why-microsoft-fabric-rti-for-youtube-stream-analytics">Why Microsoft Fabric RTI for YouTube Stream Analytics?</h2>
<p>RTI isn't just another event streaming platform—it's built for scenarios where milliseconds matter. Unlike traditional ETL pipelines that batch-process data every few hours, RTI creates a continuous data pipeline that can ingest, transform, and analyze YouTube API responses as they arrive.</p>
<p>What makes this particularly interesting is RTI's native integration with KQL (Kusto Query Language) databases, which can handle semi-structured JSON payloads from YouTube APIs without complex schema transformations. Plus, the EventStream connectors eliminate the need for custom message brokers.</p>
<p>For comprehensive fundamentals, Microsoft's official documentation provides excellent coverage: <a target="_blank" href="https://learn.microsoft.com/en-us/fabric/real-time-intelligence/">Microsoft Fabric Real-Time Intelligence Documentation</a>.</p>
<h2 id="heading-project-scope-sri-lankan-news-ecosystem">Project Scope : Sri Lankan News Ecosystem</h2>
<p>I targeted my country - Sri Lanka's major news broadcasters during their prime-time slots : Lunch (11:45-13:00), evening (18:30-19:45), and night (21:00-22:00). The hypothesis? YouTube concurrent viewers could serve as a leading indicator for overall channel engagement trends.</p>
<p>Here's the kicker : While YouTube Live represents maybe 10-20% of total TV or post streaming of Youtube Live viewership, it often shows engagement spikes 30-60 minutes before they appear in traditional viewership metrics and this leads a very crucial information of actual overall TV channel viewers preference and taste. This makes real-time YouTube analytics surprisingly valuable for media monitoring and competitive intelligence.</p>
<hr />
<h2 id="heading-architecture">Architecture :</h2>
<p>Here's how the pieces fit together :</p>
<p>The architecture leverages MS Fabric's strength : Seamless integration between compute ( PySpark notebooks), messaging (EventStreams), and analytics (KQL databases).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747855180472/ac270fc8-aadd-47df-93c1-a7f3dfd802dc.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-01-understanding-youtube-api-economics-and-workarounds">Step 01 : Understanding YouTube API Economics and Workarounds</h2>
<p>YouTube Data API v3 (Free Tier) pricing creates interesting constraints. It gives you 10,000 quota units daily, but here's the catch:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Operation</td><td>Cost</td><td>Reality Check</td></tr>
</thead>
<tbody>
<tr>
<td><code>videos.list</code></td><td>1 unit</td><td>Cheap metadata extraction</td></tr>
<tr>
<td><code>search.list</code></td><td>100 units</td><td>Expensive discovery - only 100 calls/day</td></tr>
<tr>
<td><code>liveBroadcasts.list</code></td><td>50 units</td><td>Moderate live stream lookup</td></tr>
</tbody>
</table>
</div><p>Additional info : <a target="_blank" href="https://developers.google.com/youtube/v3/getting-started">https://developers.google.com/youtube/v3/getting-started</a></p>
<p>Monitor with quota console: <a target="_blank" href="https://console.cloud.google.com/apis/api/youtube.googleapis.com/quotas">https://console.cloud.google.com/apis/api/youtube.googleapis.com/quotas</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747853772701/d6419991-d85d-454b-9d48-efc03efc5b6d.png" alt class="image--center mx-auto" /></p>
<p>For my use case (checking 8 channels every 60 seconds for 300 minutes daily), I'd need ~2,400 search operations. That's 240,000 units—24x over the free limit.</p>
<blockquote>
<p><strong>Solution</strong>: I bypassed the expensive <code>search.list</code> by scraping YouTube's <code>/streams</code> endpoint directly:</p>
<p>This drops the cost from 100 units to ~1 unit per channel check. The trade-off? You're now dependent on YouTube's HTML structure, which could break with UI updates.</p>
<p><strong>Tip</strong>: Use the hybrid approach—scrape for discovery, then use official APIs for metadata. This gives you the best of both worlds: cost efficiency and data reliability.</p>
</blockquote>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Important : YouTube API keys can be <strong>blocked</strong>, <strong>rate-limited</strong>, or <strong>suspended</strong> in several situations, especially if the usage pattern violates Google’s policies or exceeds thresholds . So be responsible.</div>
</div>

<hr />
<p>Note down API key in somewhere safe,we need this later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747854689967/45d44002-31b8-4fc9-9451-63785af9f44c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-02-eventhouse-setup-kql-database-configuration">Step 02 : Eventhouse Setup - KQL Database Configuration</h2>
<p>We create a new Eventhouse to hold the KQL Database and subsequent tables. (refer my previous blog posts which explains several different use cases with custom endpoints)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739813267338/5b13e2bc-07bb-453b-b627-c246a872c1f9.png?auto=compress,format&amp;format=webp" alt /></p>
<p>I created an Eventhouse named <code>ISS_DB_001</code> with a KQL database of the same name.In production, use descriptive meaningful names. Trust me, future-you will appreciate the clarity when managing multiple data sources.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747855604801/ec8eb77c-1ea0-46de-8e0f-32ea54077e5d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-03-setting-up-eventstream-ytlivees">Step 03 - Setting up Eventstream | YT_Live_ES</h2>
<p>Creating a new Eventstream is very straightforward ; and after that we have to add a source. Since I am planning to retrieve Youtube API data, I would use custom endpoint as my source.</p>
<p>The EventStream (<code>YT_Live_ES</code>) acts as the ingestion gateway. When you create a Custom Endpoint source, Fabric generates a Service Bus connection string—this becomes your programmatic entry point for streaming data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747856156299/ad372809-7c28-4147-8952-9054e07f9df6.png" alt class="image--center mx-auto" /></p>
<p>Once Custom endpoint added as a source we obtain the <strong>Connection string - primary key</strong> as below. (Protocol : Event Hub ),Note down this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814020516/5dfa3b26-94f9-477b-b736-2163e2396da6.png?auto=compress,format&amp;format=webp" alt /></p>
<h2 id="heading-step-3-pyspark-notebook-implementation">Step 3 - PySpark Notebook Implementation :</h2>
<p>Okay, now we need to create a notebook and start writing the code to retrieve data from the API and push the payload to the ‘YT_Live_ES’ Eventstream .</p>
<p>Let me break down the core PySpark notebook implementation, focusing on what each key section accomplishes:</p>
<h3 id="heading-configuration-setup-lines-13-17">Configuration Setup (Lines 13-17)</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747856798544/7cae1dec-2411-434e-9f77-b44aecfaedb0.png" alt class="image--center mx-auto" /></p>
<p><strong>Line 13 (100 minutes)</strong>: Since I have many channels to cover across different time belts (lunch, evening, night), I set 100 minutes to ensure complete coverage of any prime-time news slot.</p>
<p><strong>Line 14 (SAS Connection)</strong>: This is the EventStream connection string we got from the custom endpoint setup.</p>
<p><strong>Line 15 (API Key)</strong>: The YouTube API key we saved earlier for authentication.</p>
<p><strong>Line 17 (Channel Handles)</strong>: These are the Sri Lankan news channels I want to track.</p>
<p><strong>Line 20 (Skip IDs)</strong>: Some channels transmit more than one live stream simultaneously, so I skip non-news streaming IDs to focus only on news channels.</p>
<h3 id="heading-fetching-stream-details-via-api">Fetching Stream Details via API</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747887595507/1b782877-4bd9-45e0-9894-87aa507ff9ce.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747887613950/e36f3fb4-a694-4f4c-9a49-0dce309bb8bf.png" alt class="image--center mx-auto" /></p>
<p>This takes a list of video IDs, retrieves detailed information from the YouTube API, and formats it into a clean, structured dataset that includes:</p>
<ul>
<li><p>Stream metadata (title, channel, URL)</p>
</li>
<li><p>Live viewer count</p>
</li>
<li><p>Publishing and retrieval timestamps</p>
</li>
<li><p>Stream status (live or ended)</p>
</li>
</ul>
<h3 id="heading-sending-data-to-fabric-eventstream">Sending Data to Fabric EventStream</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747887680118/5c4015b7-dc6d-418c-9546-ceb3590f48e0.png" alt class="image--center mx-auto" /></p>
<p>This function handles the critical task of streaming our processed data to Microsoft Fabric's EventStream service. It:</p>
<ol>
<li><p>Extracts the entity path from the connection string</p>
</li>
<li><p>Creates a Service Bus client connection</p>
</li>
<li><p>Converts each message to JSON</p>
</li>
<li><p>Sends the batch of messages to the EventStream</p>
</li>
<li><p>Properly closes the connection to prevent resource leaks</p>
</li>
</ol>
<h3 id="heading-core-tracking-loop">Core Tracking Loop</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747887788310/18ef60c3-a854-44b8-bc74-1447c5a29fd8.png" alt class="image--center mx-auto" /></p>
<p>This block includes:</p>
<ol>
<li><p>Sets a 100-minute runtime limit</p>
</li>
<li><p>Checks each channel for live streams</p>
</li>
<li><p>Combines currently discovered streams with previously tracked ones</p>
</li>
<li><p>Fetches detailed metadata for all tracked streams</p>
</li>
<li><p>Filters for news-related content only (using both English "news" and Sinhala "ප්‍රවෘත්ති" keywords)</p>
</li>
<li><p>Updates the tracking state (adding new streams, removing ended ones)</p>
</li>
<li><p>Sends the filtered data to Fabric EventStream</p>
</li>
<li><p>Waits 60 seconds before the next cycle</p>
</li>
</ol>
<blockquote>
<p>Full code is in this <a target="_blank" href="https://gist.github.com/nalakan/8d8a3dbf826f1d618bf0a87b26361a45">github gist</a>.</p>
</blockquote>
<hr />
<h2 id="heading-summarized-flow-of-entire-notebook">Summarized Flow of Entire NoteBook <strong>:</strong></h2>
<ul>
<li><p>Defines a list of Sri Lankan news channels to monitor (<code>@HiruNewsOfficial</code>, <code>@AdaDeranaNews</code>, etc.).</p>
</li>
<li><p><strong>Getting Live Video IDs:</strong></p>
<ul>
<li><p>Fetches HTML content from the <code>/streams</code> page of each channel using <code>requests</code>.</p>
</li>
<li><p>Extracts the current or upcoming live video ID using regex.</p>
</li>
<li><p>Skips any videos explicitly marked in a <code>SKIP_VIDEO_IDS</code> set.</p>
</li>
</ul>
</li>
<li><p><strong>Fetching Stream Metadata:</strong></p>
<ul>
<li><p>Calls the <code>videos.list</code> endpoint of the YouTube API for all found video IDs.</p>
</li>
<li><p>Collects structured metadata such as title, channel name, viewer count, and live status.</p>
</li>
</ul>
</li>
<li><p><strong>Filtering Relevant Streams:</strong></p>
<ul>
<li><p>Filters only streams that are:</p>
<ul>
<li><p>Live (not ended).</p>
</li>
<li><p>Containing “news” or “ප්‍රවෘත්ති” in the video title.</p>
</li>
<li><p>Not explicitly skipped.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Sending to Microsoft Fabric EventStream:</strong></p>
<ul>
<li><p>Converts the filtered stream data into JSON payload.</p>
</li>
<li><p>Sends them to a configured Fabric EventStream.</p>
</li>
</ul>
</li>
<li><p><strong>Continuous Monitoring:</strong></p>
<ul>
<li><p>Runs for a configured time duration (default: 100 minutes).</p>
</li>
<li><p>Repeats checks every 60 seconds.</p>
</li>
<li><p>Uses in-memory tracking (<code>active_streams</code>) to handle ongoing live streams.</p>
</li>
</ul>
</li>
</ul>
<p>When executed the NB, payload is being pushed to Eventstream as we can verify below..</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747891021542/8bfa7810-ec6d-4f75-98b1-dac39890cd04.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-04-inside-eventhouse-amp-kql-db-creating-visualizations">Step 04 : Inside EventHouse &amp; KQL DB - Creating Visualizations</h3>
<p>Once our YouTube live stream data is flowing into the Fabric Eventhouse and the KQL database, it's time for the fun part – turning this real-time data into actionable visualizations. Microsoft Fabric provides two compelling approaches for creating insights: KQL Dashboards and Power BI integration. Let me walk you through how I implemented both, and why you might choose one over the other.</p>
<h2 id="heading-setting-the-table-our-data-landing-zone"><strong>Setting the Table :</strong> Our <strong>Data Landing Zone</strong></h2>
<p>After our EventStream delivers data to the KQL database, it populates a table named <code>YTubeLive0001TBL</code>. This table contains everything we need:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747857199508/c62c098c-2e58-4cd2-8285-9d3558514f35.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-approach-1-real-time-dashboards-using-kql-querysets">Approach #1 : Real-time Dashboards using KQL querysets</h3>
<p>For real-time operational monitoring where every second counts, I built a <strong>Real-time Dashboard</strong> directly in the Fabric interface. Here's how:</p>
<ol>
<li><p>From the KQL Database interface, I clicked ‘Real Time <strong>Dashboard</strong> and named it "Daily News Tracker"</p>
</li>
<li><p>For my first tile, I created a KQL query to show which channels were currently broadcasting in different time belts:</p>
</li>
<li><p>Next, I added a visualization showing real-time viewer competition across channels:</p>
</li>
</ol>
<p>During breaking news events, I could literally watch the numbers climb in real-time as viewers flocked to different channels.What makes <strong>Real-time Dashboards</strong> special is their simplicity and speed. With just a few clicks, I created a monitoring system that provides actionable insights with minimal latency. When news breaks and every minute counts, this approach shines.</p>
<p>In my case, I ended up using <strong>both</strong> approaches complementarily: <strong>Real-time Dashboards</strong> during live broadcasts for operational monitoring, and Power BI for deeper analysis and broader sharing of insights afterward.</p>
<h3 id="heading-real-time-dashboard"><strong>Real-time Dashboard</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747857358571/93195fae-b049-4103-b9e3-d0e7078e8489.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747903594074/1967c689-f9b0-4221-8da9-e340416708d2.gif" alt class="image--center mx-auto" /></p>
<p>Below is the KQL queryset I used to derive the above information—for example, the evening news belt</p>
<pre><code class="lang-python">YTubeLive0001TBL
| where retrieval_date == startofday(now() )
  <span class="hljs-keyword">and</span> retrieval_time_only between (time(<span class="hljs-number">18</span>:<span class="hljs-number">00</span>:<span class="hljs-number">00</span>) .. time(<span class="hljs-number">19</span>:<span class="hljs-number">45</span>:<span class="hljs-number">00</span>))
  <span class="hljs-keyword">and</span> (tolower(Title) has <span class="hljs-string">"news"</span> <span class="hljs-keyword">or</span> Title has <span class="hljs-string">"ප්‍රවෘත්ති"</span>) 
  <span class="hljs-keyword">and</span> status ==<span class="hljs-string">'Live'</span>
| project platform, stream_id, Title, Channel_Title, published_at, video_url, Live_Viewers, Retrieval_DT, retrieval_date, retrieval_time_only, channel_id, status, actual_end_time
| sort by Retrieval_DT desc nulls first
</code></pre>
<h3 id="heading-approach-2-power-bi-desktop-integration-the-analysis-powerhouse"><strong>Approach # 2: Power BI Desktop Integration - The Analysis Powerhouse</strong></h3>
<p>While <strong>Real-time Dashboards</strong> are perfect for real-time monitoring, I needed more sophisticated visualizations and analytical capabilities for deeper insights. This is where Power BI Desktop comes in , i used Direct Query connection mode as I need minimal latency and set page auto refresh in ever 30 seconds.</p>
<ul>
<li>Lunch Time news - Viewer trends by channel ,time on 21-05-2025</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747857558748/aa4b65b0-f9ff-4d79-abfc-1f9bb8e8dd21.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Evening Time news - Viewer trends by channel,time on 21-05-2025</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747857497907/e2a2ac46-d388-4def-88da-6c24d0852cab.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Night Time news - Viewer trends by channel,time on 21-05-2025</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747857633951/02c5c08b-ce36-472b-bb91-4bb8a24e829e.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Image showing report interactivity and slicing/filtering options using PowerBI.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747858470405/5fa59448-733a-43f1-b39c-98f3a4a69f68.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-wrapping-up-from-promise-to-production">Wrapping Up : From Promise to Production</h2>
<p>Completed relevant Project Assets in my Fabric Workspace :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747925043841/7383fa76-4bcd-4784-8e4b-a4624c70169f.png" alt class="image--center mx-auto" /></p>
<p>Building this real-time YouTube analytics system taught me that the most interesting technical challenges often come from working within constraints—API limits, HTML parsing fragility, and cost optimization. Microsoft Fabric RTI provided the infrastructure backbone that let me focus on the data logic rather than complex ETL pipelines.</p>
<p>The hybrid scraping approach proved more resilient than expected, maintaining 99%+ uptime over a month of continuous operation. For anyone building similar streaming analytics systems, I'd recommend starting with official APIs and falling back to intelligent scraping when quota limits become prohibitive.(If paid plan is not a concern)</p>
<p>The complete NoteBook code is available in <a target="_blank" href="https://gist.github.com/nalakan/8d8a3dbf826f1d618bf0a87b26361a45">this GitHub</a> link and I'd love to see what variations you build. Whether you're tracking crypto livestreams, monitoring gaming tournaments, or analyzing educational content, the core pattern—extract, stream, analyze, visualize—remains mostly consistent.</p>
<blockquote>
<p>Is this a complete representation of the total TV audience? May be not. But gives important hints.YouTube live viewers represent only a small subset. However, the real value lies in how this data acts as a real-time barometer—often signaling shifts in public sentiment or news consumption trends well before they appear in traditional ratings.</p>
<p>Spikes in live viewership can indicate increased public interest during major events such as elections, social unrest, or breaking news. By analyzing patterns over specific time periods, we can uncover early signs of audience movement and changing channel preferences—offering a window into what’s happening across society in real time.</p>
</blockquote>
<hr />
<h3 id="heading-references"><strong>References:</strong></h3>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/fabric/real-time-intelligence/">Microsoft Fabric Real-Time Intelligence Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started">YouTube Data API v3 Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://console.cloud.google.com/apis/api/youtube.googleapis.com/quotas">Google Cloud API Quotas Console</a></p>
</li>
<li><p><a target="_blank" href="https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/">KQL Query Language Reference</a></p>
</li>
</ul>
<p><a target="_blank" href="https://www.linkedin.com/in/nalakan/"><strong>Connect with me</strong></a> to discuss real-time analytics, Microsoft Fabric implementations, or media technology innovations. I'd love to hear about your own projects in this space !</p>
<p><em>Have you implemented similar real-time analytics solutions? What challenges did you encounter, and how did you solve them? Share your experiences in the comments below.</em></p>
<p>As always Thanks for Reading - BIDiaries 😊!!!</p>
]]></content:encoded></item><item><title><![CDATA[Syncing Existing GitHub Repositories to Fabric with Semantic Link Labs / Sempy]]></title><description><![CDATA[Managing and syncing Microsoft Fabric workspaces with GitHub repositories is an essential practice for ensuring proper version control, seamless collaboration, and efficient data management. Previously, Sandeep Pawar blogged about how to sync Fabric ...]]></description><link>https://bidiaries.com/syncing-existing-github-repositories-to-fabric-with-semantic-link-labs</link><guid isPermaLink="true">https://bidiaries.com/syncing-existing-github-repositories-to-fabric-with-semantic-link-labs</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[sempy]]></category><category><![CDATA[semantic-link]]></category><category><![CDATA[semantic link labs]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[data]]></category><category><![CDATA[Python]]></category><category><![CDATA[automation]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Fri, 14 Mar 2025 17:47:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/G2lgiBBzeEM/upload/d5d811b17f0831e43d555a8727201c86.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Managing and syncing Microsoft Fabric workspaces with GitHub repositories is an essential practice for ensuring proper version control, seamless collaboration, and efficient data management. Previously, <a target="_blank" href="https://fabric.guru/sync-multiple-https://fabric.guru/sync-multiple-fabric-workspaces-with-github-using-semantic-link-labs">Sandeep Pawar</a> blogged about how to sync Fabric Workspace artifacts to GitHub programmatically.</p>
<p>All good. But what if you need to do the reverse—sync existing GitHub repositories back into Fabric? In this blog, as a sequel to Sandeep’s post, I am explaining how I achieved that.</p>
<blockquote>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This approach is particularly beneficial when an organization undergoes changes, such as onboarding new users or setting up a new capacity. Instead of manually identifying and syncing each repository one by one, this automated solution ensures that all relevant Fabric artifacts from GitHub are seamlessly restored.</div>
</div>

</blockquote>
<p>Thanks to the power of <strong>Michael Kovalsky’s</strong> <a target="_blank" href="https://semantic-link-labs.readthedocs.io/en/stable/index.html"><strong>Semantic Link Labs</strong></a> and <strong>Sempy</strong>, we can automate this process and make it efficient. In this blog, we’ll walk through how to accomplish this in a structured and automated way.</p>
<hr />
<h2 id="heading-what-i-have-done-previously">What I have done previously</h2>
<p>In my case, I previously synced my existing Fabric Workspaces to GitHub with the <mark>‘</mark><code>Bkp_</code><mark>'</mark> prefix (Refer to <a target="_blank" href="https://fabric.guru/sync-multiple-fabric-workspaces-with-github-using-semantic-link-labs">Sandeep’s previous article</a> )</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741972853174/2c6cea1f-a1d1-4760-b4bd-ceeb0fd33f4e.png" alt="Synced Workspaces to Github" class="image--center mx-auto" /></p>
<blockquote>
<p>Now, I can easily filter out these repositories and sync them into Fabric without the prefix (I created the new Workspaces with the same name in Github repos without the <mark>‘Bkp_’</mark> prefix programmatically). If a workspace already exists, I simply skip creating it and proceed with Git initialization.</p>
</blockquote>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive into the process, ensure you have:</p>
<ol>
<li><p><strong>GitHub Personal Access Token (PAT):</strong> Required for authentication with the GitHub API. Create a classic token with appropriate scopes.</p>
</li>
<li><p><strong>Microsoft Fabric Connection ID:</strong> This is generated when setting up a GitHub connection in Fabric.</p>
</li>
<li><p><strong>Administrative Rights:</strong> You must have admin access to Fabric workspaces to sync repositories.</p>
<pre><code class="lang-python"> pip install PyGithub semantic-link-labs
</code></pre>
</li>
</ol>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Before executing the Notebook code, the following shows that I don’t have any workspaces except the workspaces that I created for storing the executing Notebook.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741973091099/84b8e74e-80d5-4bfe-b987-759f81dfc0de.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-key-functions-used-in-semantic-link-labs-and-sempy">Key Functions used in Semantic Link Labs and Sempy</h2>
<p>To effectively sync Fabric workspaces with GitHub, we rely on several key functions from <strong>Semantic Link Labs</strong> and <strong>Sempy</strong>:</p>
<h3 id="heading-semantic-link-labs-functions"><strong>Semantic Link Labs Functions</strong></h3>
<ol>
<li><p><strong>labs.admin.list_git_connections()</strong></p>
<ul>
<li>Fetches all existing GitHub connections linked to Fabric workspaces.</li>
</ul>
</li>
<li><p><strong>labs.connect_workspace_to_github(owner_name, repository_name, branch_name, directory_name, connection_id, workspace)</strong></p>
<ul>
<li>Establishes a Git connection between a Fabric workspace and a GitHub repository.</li>
</ul>
</li>
<li><p><strong>labs.initialize_git_connection(workspace)</strong></p>
<ul>
<li>Initializes the GitHub connection for a Fabric workspace and returns the latest commit hash.</li>
</ul>
</li>
<li><p><strong>labs.update_from_git(workspace, remote_commit_hash, conflict_resolution_policy='PreferRemote')</strong></p>
<ul>
<li>Synchronizes the Fabric workspace with the latest version from GitHub.</li>
</ul>
</li>
</ol>
<h3 id="heading-sempy-functions"><strong>Sempy Functions</strong></h3>
<ol>
<li><p><strong>fabric.list_workspaces()</strong></p>
<ul>
<li>Retrieves a list of existing workspaces in Microsoft Fabric.</li>
</ul>
</li>
<li><p><strong>fabric.create_workspace(display_name, description, capacity_id)</strong></p>
<ul>
<li>Creates a new Fabric workspace with the specified name, description, and capacity.</li>
</ul>
</li>
</ol>
<p>These functions form the backbone of our automation, enabling us to efficiently manage workspaces and maintain synchronization between GitHub and Fabric.</p>
<hr />
<h2 id="heading-step-by-step-guide">Step-by-Step Guide</h2>
<h3 id="heading-1-initialize-github-and-fabric-connections">1. Initialize GitHub and Fabric Connections</h3>
<p>First, we set up our connections to GitHub and Microsoft Fabric:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> github <span class="hljs-keyword">import</span> Github
<span class="hljs-keyword">import</span> sempy.fabric <span class="hljs-keyword">as</span> fabric
<span class="hljs-keyword">import</span> sempy_labs <span class="hljs-keyword">as</span> labs
<span class="hljs-keyword">import</span> time

<span class="hljs-comment"># GitHub Personal Access Token and Connection ID</span>
g = Github(<span class="hljs-string">"your_github_pat"</span>)
conn_id = <span class="hljs-string">"your_connection_id"</span>
capacity_id = <span class="hljs-string">"your_capacity_id"</span>
</code></pre>
<h3 id="heading-2-fetch-existing-github-repositories">2. Fetch Existing GitHub Repositories</h3>
<p>To retrieve all repositories associated with your GitHub account:</p>
<pre><code class="lang-python">user = g.get_user()
repos = list(user.get_repos())  <span class="hljs-comment"># Convert iterator to list</span>
print(<span class="hljs-string">f"ℹ️ Total repositories found: <span class="hljs-subst">{len(repos)}</span>"</span>)
</code></pre>
<h3 id="heading-3-list-existing-workspaces-and-git-connections">3. List Existing Workspaces and Git Connections</h3>
<p>Before creating new Fabric workspaces, check which ones already exist:</p>
<pre><code class="lang-python">existing_workspaces_df = fabric.list_workspaces()
git_connections_df = labs.admin.list_git_connections()
</code></pre>
<h3 id="heading-4-automate-workspace-creation-and-connect-ws-to-git">4. Automate Workspace Creation and connect WS to Git</h3>
<p>For each repository (With “Bkp_”), we check if a corresponding workspace exists. If not, we create it and sync it with GitHub.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Initialize counters</span>
total_repos_processed = <span class="hljs-number">0</span>
workspaces_created = <span class="hljs-number">0</span>
workspaces_skipped = <span class="hljs-number">0</span>
workspaces_already_exist = <span class="hljs-number">0</span>
git_connections_created = <span class="hljs-number">0</span>
git_connections_skipped = <span class="hljs-number">0</span>
workspaces_synced_from_github = <span class="hljs-number">0</span>

<span class="hljs-keyword">for</span> repo <span class="hljs-keyword">in</span> repos:
    total_repos_processed += <span class="hljs-number">1</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> repo.name.startswith(<span class="hljs-string">"Bkp_"</span>):
            print(<span class="hljs-string">f"⚠️ Skipping '<span class="hljs-subst">{repo.name}</span>', does not match 'Bkp_' prefix."</span>)
            workspaces_skipped += <span class="hljs-number">1</span>
            <span class="hljs-keyword">continue</span>

        workspace_name = repo.name.replace(<span class="hljs-string">"Bkp_"</span>, <span class="hljs-string">""</span>)
        print(<span class="hljs-string">f"Processing repo: <span class="hljs-subst">{repo.name}</span> -&gt; Workspace name: <span class="hljs-subst">{workspace_name}</span>"</span>)

        existing_workspace = existing_workspaces_df[existing_workspaces_df[<span class="hljs-string">'Name'</span>] == workspace_name]

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> existing_workspace.empty:
            workspace_id = existing_workspace.iloc[<span class="hljs-number">0</span>][<span class="hljs-string">'Id'</span>]
            print(<span class="hljs-string">f"ℹ️ Workspace '<span class="hljs-subst">{workspace_name}</span>' already exists with ID: <span class="hljs-subst">{workspace_id}</span>."</span>)
            workspaces_already_exist += <span class="hljs-number">1</span>

            <span class="hljs-keyword">if</span> workspace_id <span class="hljs-keyword">in</span> git_connections_df[<span class="hljs-string">'Workspace Id'</span>].values:
                print(<span class="hljs-string">f"⚠️ Workspace '<span class="hljs-subst">{workspace_name}</span>' already has a Git connection. Skipping."</span>)
                git_connections_skipped += <span class="hljs-number">1</span>
                <span class="hljs-keyword">continue</span>
        <span class="hljs-keyword">else</span>:
            workspace_id = fabric.create_workspace(
                display_name=workspace_name,
                description=<span class="hljs-string">f"Workspace for <span class="hljs-subst">{repo.name}</span> synced from GitHub"</span>,
                capacity_id=capacity_id
            )
            print(<span class="hljs-string">f"🟢 Workspace '<span class="hljs-subst">{workspace_name}</span>' created with ID: <span class="hljs-subst">{workspace_id}</span>."</span>)
            workspaces_created += <span class="hljs-number">1</span>

        labs.connect_workspace_to_github(
            owner_name=user.login,
            repository_name=repo.name,
            branch_name=<span class="hljs-string">"main"</span>,
            directory_name=<span class="hljs-string">"/"</span>,
            connection_id=conn_id,
            workspace=workspace_id
        )
        print(<span class="hljs-string">f"🟢 Connected workspace '<span class="hljs-subst">{workspace_name}</span>' to GitHub repository '<span class="hljs-subst">{repo.name}</span>'."</span>)
        git_connections_created += <span class="hljs-number">1</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"⚠️ Unexpected error processing '<span class="hljs-subst">{repo.name}</span>': <span class="hljs-subst">{e}</span>"</span>)
</code></pre>
<hr />
<h3 id="heading-4-initialize-git-connection-and-git-pull">4. Initialize Git Connection and Git pull</h3>
<pre><code class="lang-python">        <span class="hljs-comment"># Wait for Git connection to initialize</span>
        time.sleep(<span class="hljs-number">10</span>)  <span class="hljs-comment"># Adjust as necessary</span>
        <span class="hljs-keyword">try</span>:
            commit_hash = labs.initialize_git_connection(workspace=workspace_id)
            print(<span class="hljs-string">f"🟢 Git connection initialized for '<span class="hljs-subst">{workspace_name}</span>'. Commit: <span class="hljs-subst">{commit_hash}</span>"</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"⚠️ Error initializing Git for '<span class="hljs-subst">{workspace_name}</span>': <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">continue</span>  <span class="hljs-comment"># Move to the next repo</span>

        <span class="hljs-comment"># Automatically update workspace from Git</span>
        <span class="hljs-keyword">try</span>:
            update_status = labs.update_from_git(workspace=workspace_id, remote_commit_hash=commit_hash, conflict_resolution_policy=<span class="hljs-string">'PreferRemote'</span>)
            print(<span class="hljs-string">f"🟢 Updated workspace '<span class="hljs-subst">{workspace_name}</span>' from Git. Status: <span class="hljs-subst">{update_status}</span>"</span>)
            workspaces_synced_from_github += <span class="hljs-number">1</span>
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"⚠️ Error updating workspace '<span class="hljs-subst">{workspace_name}</span>' from Git: <span class="hljs-subst">{e}</span>"</span>)

        <span class="hljs-comment"># Sync all content from GitHub repo</span>
        print(<span class="hljs-string">f"🟢 Workspace '<span class="hljs-subst">{workspace_name}</span>' setup completed. Moving to next repo."</span>)

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"⚠️ Unexpected error processing '<span class="hljs-subst">{repo.name}</span>': <span class="hljs-subst">{e}</span>"</span>
</code></pre>
<h3 id="heading-output-of-the-code">Output of the code</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741973601243/ce365a4f-3953-4aa1-a200-146ca8380f0a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741973839792/246f4543-5e65-4f33-8d16-31531956c504.png" alt /></p>
<hr />
<p>Workspaces are now automatically created and content is synced in Fabric.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741974008582/fce754a2-e6a1-441d-ae35-33f549997167.png" alt class="image--center mx-auto" /></p>
<hr />
<p>Following Workspace pulled the content from Github Repo and now it is connected with Github automatically.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742121695486/c143ae59-1d62-4151-9c7e-571ad65c305e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>With these steps, you can now integrate previously synced Github artifacts into Microsoft Fabric with minimal effort.(No manual configuration). For Azure DevOps repos the process would be the same.</p>
<p><strong>Full code is available in this</strong> <a target="_blank" href="https://gist.github.com/nalakan/dcc62b093b7d69482cb8780aa349aac6"><strong>GitHub Gist</strong></a><strong>.</strong></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="dcc62b093b7d69482cb8780aa349aac6"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/nalakan/dcc62b093b7d69482cb8780aa349aac6" class="embed-card">https://gist.github.com/nalakan/dcc62b093b7d69482cb8780aa349aac6</a></div><p> </p>
<p>Happy syncing - Thanks for Reading!</p>
]]></content:encoded></item><item><title><![CDATA[Real-Time Intelligence with PySpark / Python Notebooks and Power BI –
The Perfect Trio for Tracking the International Space Station (ISS)]]></title><description><![CDATA[Introduction
Did you know that astronauts aboard the International Space Station (ISS) witness 16 sunrises and sunsets every day?
Travelling at 28,000 km/h (17,500 mph), the ISS completes 15.5 orbits around Earth daily, making it one of the most fasc...]]></description><link>https://bidiaries.com/real-time-intelligence-with-pyspark-python-notebooks-and-power-bi-the-perfect-trio-for-tracking-the-international-space-station-iss</link><guid isPermaLink="true">https://bidiaries.com/real-time-intelligence-with-pyspark-python-notebooks-and-power-bi-the-perfect-trio-for-tracking-the-international-space-station-iss</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[real time analytics]]></category><category><![CDATA[Real Time]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI Training ]]></category><category><![CDATA[Consultancy]]></category><category><![CDATA[fabric]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[space]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 18 Feb 2025 15:26:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/wAkLQnT2TC0/upload/b1b014790a9ec4a4029be76ad2aacb41.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Did you know that astronauts aboard the <strong>International Space Station (ISS)</strong> witness <strong>16 sunrises and sunsets</strong> every day?</p>
<p>Travelling at <strong>28,000 km/h (17,500 mph)</strong>, the ISS completes <strong>15.5 orbits around Earth daily</strong>, making it one of the most fascinating engineering marvels in human history. As of February 2025, the station has hosted over 270 astronauts from 19 countries, symbolizing a remarkable achievement in human ingenuity and cooperation.</p>
<p>This blog explores how <strong>real-time streaming intelligence</strong> can be achieved using <strong>Microsoft Fabric Python/PySpark notebooks, event streams, and KQL databases and my ever loving tool PowerBI</strong>. Rather than just building another ISS tracking report/dashboard, the focus is on showcasing the power of <strong>real-time data processing</strong> and end-to-end analytics within Fabric.</p>
<p>This work is inspired by many prior implementations, including those by <strong>Anshul Sharma and Bradley Ball @ Microsoft</strong>, who demonstrated ISS tracking with <strong>Azure Logic Apps</strong>. Kudos also to <strong>Vahid DM (Microsoft MVP )</strong> for his <a target="_blank" href="https://www.vahiddm.com/post/sending-api-data-to-fabric-real-time-intelligence-with-notebooks">article</a>, which provided valuable insights on the same topic for API integration with Notebooks and Eventstream. Expanding on these foundations, this approach introduces <strong>new techniques</strong> that enhance flexibility and efficiency using <strong>custom endpoints and event-streaming without Azure Logic Apps.</strong></p>
<p>Let’s dive in and see how we can track the ISS and its astronauts in real-time / near real-time 😊</p>
<h2 id="heading-system-architecture">System Architecture</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739868196034/ed2a6bf6-cbc0-4153-9f6a-10dad8b29bb9.gif" alt="Click to zoom" class="image--center mx-auto" /></p>
<p>To acquire data on the International Space Station (ISS), we utilize two APIs: the ISS Location API and the Astronauts Data API. We process this data using separate Python or PySpark notebooks, which send the payload to an event stream via custom endpoints. Subsequently, the streamed data is stored as tables in the Eventhouse KQL Database, serving as the foundation for our Power BI reports and dashboards.</p>
<h3 id="heading-step-01">Step 01</h3>
<p>First, we create a new Eventhouse to hold the KQL Database and subsequent tables.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739813267338/5b13e2bc-07bb-453b-b627-c246a872c1f9.png" alt class="image--center mx-auto" /></p>
<p>In my setup, the Eventhouse is named 'ISS_Eventhouse,' and the KQL Database is named 'ISS_DB_001.' Adopting a meaningful naming convention is always a best practice that enhances clarity and maintainability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739813502483/db438cb9-ca45-4987-bccb-f65a4aca5b81.png" alt class="image--center mx-auto" /></p>
<p>Since we are working with two distinct data streams—one for ‘ISS Geo location’ and the other for ‘ISS Astronauts’—we need to create two separate tables to store this data. For now, we'll focus on setting up the Eventstream and leave the table creation on hold. As you progress, you'll understand why it's essential to first establish the Eventhouse and KQL Database before moving forward with the Eventstream setup.</p>
<h3 id="heading-step-02-setting-up-eventstream-01-issgeolocationes">Step 02 - Setting up Eventstream 01 | ISS_Geo_Location_ES</h3>
<p>Creating a new Eventstream is very straightforward ; and after that we have to add a source. In this exercisE since I am planning to get ‘ISS Geo’ API data I would use custom endpoint as my source (Refer to my previous blog post on comprehensive use case with Custom endpoint)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739813864157/11c8dd60-b3f4-454d-91d7-7df4991748a6.png" alt class="image--center mx-auto" /></p>
<p>Once Custom endpoint added as a source we can get the SAS Key <strong>(Connection string - primary key)</strong> as below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814020516/5dfa3b26-94f9-477b-b736-2163e2396da6.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-3-code-execution-via-pyspark-python-notebook">Step 3 - Code execution via Pyspark/ Python Notebook</h3>
<p>Okay, now we need to switch to our notebook and start writing the code to retrieve data from the API and push the payload to the ‘ISS_Geo_Location_ES’ Eventstream . First, we need to install the following libraries .</p>
<ul>
<li><p><strong>requests</strong>: Used to send HTTP requests to the API and fetch live data.</p>
</li>
<li><p><strong>pytz</strong>: Helps manage time zones and convert timestamps. In this blog, we’ll use Sri Lanka,Colombo Time.</p>
</li>
<li><p><strong>azure-servicebus</strong>: Allows us to send messages from the Notebook to Fabric’s EventStream.</p>
</li>
</ul>
<pre><code class="lang-python">pip install requests pytz azure-servicebus
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> azure.servicebus <span class="hljs-keyword">import</span> ServiceBusClient, ServiceBusMessage
<span class="hljs-keyword">import</span> pytz

<span class="hljs-comment"># Replace with your Fabric EventStream connection string</span>
myconnectionstring = <span class="hljs-string">"YOUR SAS KEY"</span>

<span class="hljs-comment"># API URL - Change this to any API you want to use</span>
API_URL = <span class="hljs-string">"http://api.open-notify.org/iss-now.json"</span>  
<span class="hljs-comment"># API_URL = "https://api.wheretheiss.at/v1/satellites/25544"  # Example: ISS API</span>

<span class="hljs-comment"># Function to fetch data from any API</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_api_data</span>():</span>
    <span class="hljs-keyword">try</span>:
        response = requests.get(API_URL)
        response.raise_for_status()  <span class="hljs-comment"># Raises an error if the request fails</span>
        data = response.json()

        <span class="hljs-comment"># Convert single object response to a list for consistency</span>
        <span class="hljs-keyword">if</span> isinstance(data, dict):
            <span class="hljs-keyword">return</span> [data]  <span class="hljs-comment"># Wrap single dictionary in a list</span>

        <span class="hljs-keyword">return</span> data  <span class="hljs-comment"># Return list as-is</span>
    <span class="hljs-keyword">except</span> requests.exceptions.RequestException <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error fetching data: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

<span class="hljs-comment"># Function to add timestamps in Sri Lanka (Colombo) time with explicit UTC +5:30</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_timestamps</span>(<span class="hljs-params">data</span>):</span>
    colombo_tz = pytz.timezone(<span class="hljs-string">"Asia/Colombo"</span>)
    now_colombo = datetime.now(colombo_tz)

    formatted_datetime = now_colombo.strftime(<span class="hljs-string">"%m/%d/%Y %I:%M:%S %p"</span>) + <span class="hljs-string">" UTC +5:30"</span>  <span class="hljs-comment"># MM/DD/YYYY HH:MM:SS AM/PM UTC +5:30</span>
    date_column = now_colombo.strftime(<span class="hljs-string">"%d-%m-%Y"</span>)  <span class="hljs-comment"># DD-MM-YYYY</span>
    time_column = now_colombo.strftime(<span class="hljs-string">"%H:%M:%S"</span>)  <span class="hljs-comment"># HH:MM:SS</span>
    utc_offset = <span class="hljs-string">"UTC +5:30"</span>  <span class="hljs-comment"># Explicitly set the offset</span>

    <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> data:
        record[<span class="hljs-string">"datetime"</span>] = formatted_datetime  <span class="hljs-comment"># Full timestamp with explicit UTC +5:30</span>
        record[<span class="hljs-string">"date"</span>] = date_column  <span class="hljs-comment"># Just the date</span>
        record[<span class="hljs-string">"time"</span>] = time_column  <span class="hljs-comment"># Just the time</span>
        record[<span class="hljs-string">"utc_offset"</span>] = utc_offset  <span class="hljs-comment"># Separate UTC offset for reference</span>

    <span class="hljs-keyword">return</span> data


<span class="hljs-comment"># Function to send processed data to Microsoft Fabric EventStream</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_to_eventstream</span>(<span class="hljs-params">messages, connection_string</span>):</span>
    <span class="hljs-comment"># Extract EntityPath from connection string</span>
    entity_path = <span class="hljs-literal">None</span>
    <span class="hljs-keyword">for</span> param <span class="hljs-keyword">in</span> connection_string.split(<span class="hljs-string">';'</span>):
        <span class="hljs-keyword">if</span> param.startswith(<span class="hljs-string">'EntityPath='</span>):
            entity_path = param.split(<span class="hljs-string">'='</span>)[<span class="hljs-number">1</span>]
            <span class="hljs-keyword">break</span>

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> entity_path:
        <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"EntityPath not found in connection string. Please check your connection details."</span>)

    <span class="hljs-comment"># Ensure data is always a list before sending</span>
    <span class="hljs-keyword">if</span> isinstance(messages, dict):
        messages = [messages]  <span class="hljs-comment"># Convert single object to a list</span>

    <span class="hljs-comment"># Establish connection to Fabric EventStream</span>
    servicebus_client = ServiceBusClient.from_connection_string(connection_string)
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> servicebus_client.get_queue_sender(entity_path) <span class="hljs-keyword">as</span> sender:
            <span class="hljs-comment"># Convert messages to JSON format</span>
            batch_message = [ServiceBusMessage(json.dumps(msg)) <span class="hljs-keyword">for</span> msg <span class="hljs-keyword">in</span> messages]
            sender.send_messages(batch_message)
            print(<span class="hljs-string">f"Successfully sent <span class="hljs-subst">{len(messages)}</span> records to EventStream."</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error sending messages: <span class="hljs-subst">{e}</span>"</span>)
    <span class="hljs-keyword">finally</span>:
        servicebus_client.close()

<span class="hljs-comment"># Infinite loop to fetch and send data every 2 seconds</span>
print(<span class="hljs-string">f"Starting real-time data streaming from <span class="hljs-subst">{API_URL}</span> to Fabric..."</span>)
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    data = fetch_api_data()  <span class="hljs-comment"># Fetch data from API</span>
    <span class="hljs-keyword">if</span> data:
        processed_data = add_timestamps(data)  <span class="hljs-comment"># Add date and time</span>
        send_to_eventstream(processed_data, myconnectionstring)  <span class="hljs-comment"># Send data to EventStream</span>
        print(<span class="hljs-string">f"Sent <span class="hljs-subst">{len(processed_data)}</span> records at <span class="hljs-subst">{processed_data[<span class="hljs-number">0</span>][<span class="hljs-string">'datetime'</span>]}</span>"</span>)
    time.sleep(<span class="hljs-number">2</span>)  <span class="hljs-comment"># Wait for 2 seconds before fetching new data</span>
</code></pre>
<p>Here, we are using the following API to retrieve the current location of the ISS : <code>"</code><a target="_blank" href="http://api.open-notify.org/iss-now.json"><code>http://api.open-notify.org/iss-now.json</code></a><code>"</code>.</p>
<blockquote>
<p>This code block continuously fetches data from an above API (tracking the International Space Station's location), adds timestamps in Sri Lanka's time zone (UTC +5:30) (My current Location), and sends the processed data to Microsoft Fabric’s EventStream every 2 seconds.</p>
<h3 id="heading-key-functions"><strong>Key Functions:</strong></h3>
<ol>
<li><p><strong>fetch_api_data()</strong> – Retrieves data from the specified API.</p>
</li>
<li><p><strong>add_timestamps()</strong> – Adds the current date and time in Colombo (Sri Lanka) time zone.</p>
</li>
<li><p><strong>send_to_eventstream()</strong> – Sends the processed data to Microsoft Fabric EventStream using Azure Service Bus.</p>
</li>
</ol>
<p>The script runs indefinitely, pulling live data every 2 seconds and streaming it to Fabric.</p>
</blockquote>
<p>After running the code above, we should receive a confirmation message indicating that the payload has been successfully ingested into the EventStream.I tested this with both Pyspark and Python notebooks in Fabric ,both worked well,Since the payload is quite small for a tumbling window I used Python Notebooks for the rest of the demo.Below figure shows the successful ingestion to EventStream.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814383257/74a1fb19-437c-43c9-9a91-d2ecbc1a9bc2.png" alt class="image--center mx-auto" /></p>
<p>If we go back to EventStream, we can see the incoming data stream for ‘<mark>ISS Geo Location</mark>’ as below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814487675/f211c2c1-70f2-4916-9c72-4490ff090a5c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-04-creating-tables-in-eventhouse-kql-db">Step 04 -Creating tables in Eventhouse KQL DB.</h3>
<p>As you remember in Step 01 ,we created a KQL Database named ‘ISS_DB_001’ . In order to create a table to save the data receiving from Eventstream we select ‘Get data’ option and select ‘Existing Eventstream’ as below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814694797/f6bf70ff-aede-4124-9f4a-77a8b8adba85.png" alt class="image--center mx-auto" /></p>
<p>Once select that, we can choose to save a table name , in this case I chosen as ‘ISSGeoLocation’ and configured the rest as it is.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739814789224/e4873ce3-70ff-4032-a01a-ac58e4f20791.png" alt class="image--center mx-auto" /></p>
<p>Once we finish this step and confirm that everything is working as expected, we can verify that the following flow in Eventstream has been correctly mapped in the Eventstream(ISS_Geo_Location_ES) canvas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739815263624/03f84aa8-cc5c-422a-b96f-ad56118f832b.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-05-setting-up-eventstream-02-issgeoastronautses">Step 05 - Setting up Eventstream 02 | ISS_Geo_Astronauts_ES</h3>
<p>Similar pattern continues for Eventstream 02 and rest as above .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739815659781/6c116692-fcac-4088-82c7-4322771d716f.png" alt class="image--center mx-auto" /></p>
<p>Locating the SAS Key for use of python Notebook as below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739815723161/75bb5255-9847-4b1d-85fd-53c6a0b9660a.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>This time we use the API for Astronauts data “API_URL = "http://api.open-notify.org/astros.json"</p>
</blockquote>
<h3 id="heading-step-05-code-execution-via-pyspark-python-notebook">Step 05 - Code execution via Pyspark/ Python Notebook</h3>
<blockquote>
<p>Only change Aare the API URL and SAS KEY ,others are same.</p>
</blockquote>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> azure.servicebus <span class="hljs-keyword">import</span> ServiceBusClient, ServiceBusMessage
<span class="hljs-keyword">import</span> pytz

<span class="hljs-comment"># Replace with your Fabric EventStream connection string</span>
myconnectionstring = <span class="hljs-string">"YOUR SAS KEY"</span>

<span class="hljs-comment"># API URL - Change this to any API you want to use</span>
API_URL = <span class="hljs-string">"http://api.open-notify.org/astros.json"</span>  
<span class="hljs-comment"># API_URL = "https://api.wheretheiss.at/v1/satellites/25544"  # Example: ISS API</span>

<span class="hljs-comment"># Function to fetch data from any API</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_api_data</span>():</span>
    <span class="hljs-keyword">try</span>:
        response = requests.get(API_URL)
        response.raise_for_status()  <span class="hljs-comment"># Raises an error if the request fails</span>
        data = response.json()

        <span class="hljs-comment"># Convert single object response to a list for consistency</span>
        <span class="hljs-keyword">if</span> isinstance(data, dict):
            <span class="hljs-keyword">return</span> [data]  <span class="hljs-comment"># Wrap single dictionary in a list</span>

        <span class="hljs-keyword">return</span> data  <span class="hljs-comment"># Return list as-is</span>
    <span class="hljs-keyword">except</span> requests.exceptions.RequestException <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error fetching data: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

<span class="hljs-comment"># Function to add timestamps in Sri Lanka (Colombo) time</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_timestamps</span>(<span class="hljs-params">data</span>):</span>
    colombo_tz = pytz.timezone(<span class="hljs-string">"Asia/Colombo"</span>)
    now_colombo = datetime.now(colombo_tz)

    formatted_datetime = now_colombo.strftime(<span class="hljs-string">"%m/%d/%Y %I:%M:%S %p"</span>)  <span class="hljs-comment"># MM/DD/YYYY HH:MM:SS AM/PM</span>
    date_column = now_colombo.strftime(<span class="hljs-string">"%d-%m-%Y"</span>)  <span class="hljs-comment"># DD-MM-YYYY</span>
    time_column = now_colombo.strftime(<span class="hljs-string">"%H:%M:%S"</span>)  <span class="hljs-comment"># HH:MM:SS</span>

    <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> data:
        record[<span class="hljs-string">"datetime"</span>] = formatted_datetime
        record[<span class="hljs-string">"date"</span>] = date_column
        record[<span class="hljs-string">"time"</span>] = time_column

    <span class="hljs-keyword">return</span> data

<span class="hljs-comment"># Function to send processed data to Microsoft Fabric EventStream</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_to_eventstream</span>(<span class="hljs-params">messages, connection_string</span>):</span>
    <span class="hljs-comment"># Extract EntityPath from connection string</span>
    entity_path = <span class="hljs-literal">None</span>
    <span class="hljs-keyword">for</span> param <span class="hljs-keyword">in</span> connection_string.split(<span class="hljs-string">';'</span>):
        <span class="hljs-keyword">if</span> param.startswith(<span class="hljs-string">'EntityPath='</span>):
            entity_path = param.split(<span class="hljs-string">'='</span>)[<span class="hljs-number">1</span>]
            <span class="hljs-keyword">break</span>

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> entity_path:
        <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"EntityPath not found in connection string. Please check your connection details."</span>)

    <span class="hljs-comment"># Ensure data is always a list before sending</span>
    <span class="hljs-keyword">if</span> isinstance(messages, dict):
        messages = [messages]  <span class="hljs-comment"># Convert single object to a list</span>

    <span class="hljs-comment"># Establish connection to Fabric EventStream</span>
    servicebus_client = ServiceBusClient.from_connection_string(connection_string)
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> servicebus_client.get_queue_sender(entity_path) <span class="hljs-keyword">as</span> sender:
            <span class="hljs-comment"># Convert messages to JSON format</span>
            batch_message = [ServiceBusMessage(json.dumps(msg)) <span class="hljs-keyword">for</span> msg <span class="hljs-keyword">in</span> messages]
            sender.send_messages(batch_message)
            print(<span class="hljs-string">f"Successfully sent <span class="hljs-subst">{len(messages)}</span> records to EventStream."</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error sending messages: <span class="hljs-subst">{e}</span>"</span>)
    <span class="hljs-keyword">finally</span>:
        servicebus_client.close()

<span class="hljs-comment"># Infinite loop to fetch and send data every 2 seconds</span>
print(<span class="hljs-string">f"Starting real-time data streaming from <span class="hljs-subst">{API_URL}</span> to Fabric..."</span>)
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    data = fetch_api_data()  <span class="hljs-comment"># Fetch data from API</span>
    <span class="hljs-keyword">if</span> data:
        processed_data = add_timestamps(data)  <span class="hljs-comment"># Add date and time</span>
        send_to_eventstream(processed_data, myconnectionstring)  <span class="hljs-comment"># Send data to EventStream</span>
        print(<span class="hljs-string">f"Sent <span class="hljs-subst">{len(processed_data)}</span> records at <span class="hljs-subst">{processed_data[<span class="hljs-number">0</span>][<span class="hljs-string">'datetime'</span>]}</span>"</span>)
    time.sleep(<span class="hljs-number">2</span>)  <span class="hljs-comment"># Wait for 2 seconds before fetching new data</span>
</code></pre>
<p>Eventstream 02 get the payload of Astronauts data as below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739816019396/49d5e2e2-5e78-4e94-b5c1-38c1093c4c0c.png" alt class="image--center mx-auto" /></p>
<p>Similarly, for astronauts, we create a new table in the KQL database for astronaut data as ‘ISSAstronauts’.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739816134081/30927648-df9d-4850-acf4-8ab667b43889.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>With this step completed, our work with Eventstream and Notebooks are concluded (Notebooks needs to be scheduled). The remaining tasks involve the KQL database, KQL Magics, and Power BI.</p>
</blockquote>
<h3 id="heading-step-06-design-kql-query-sets-and-apply-transformations">Step 06 - Design KQL query sets and apply transformations.</h3>
<p>The KQL queries that have been used I have attached follows.These queries were first tested in KQL DB and then used inside the PowerBI and setting up the storage mode as <mark>Direct query</mark>.</p>
<pre><code class="lang-plaintext">// Code for the ISS latest tragectory -100 Time Points

ISSGeoLocation
| top 100 by timestamp
| project iss_position_longitude, iss_position_latitude, timestamp
| render scatterchart with ( kind=map )
</code></pre>
<pre><code class="lang-plaintext">// ISS Geo location in Past 90 Minutes

ISSGeoLocation
| extend Timestamp=todatetime(timestamp)
| where Timestamp &gt; ago(90m)
| project iss_position_longitude, iss_position_latitude, Timestamp
</code></pre>
<pre><code class="lang-plaintext">// Details of ISSAstronauts

ISSAstronauts
| top 1 by ['datetime']
| mv-expand people
| project Name= people.name, Craft=people.craft
</code></pre>
<blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742889482125/36d9ae27-5cc3-4f74-b4df-f530a1efbd78.png" alt class="image--center mx-auto" /></p>
<p>Obtain the Ingestion URI by copying as above in KQL DB.This will be our cluster URL for ADX / Kusto source.</p>
</blockquote>
<h3 id="heading-step-07-connecting-the-dots-in-powerbi">Step 07 - Connecting the dots in PowerBI</h3>
<p>We then use the same KQL queries and use as a <mark>Directquery</mark> storage mode in PowerBI in order to see the latest and greatest data from KQL DB tables.</p>
<blockquote>
<p>ISS Location Table -Current Live Location of ISS</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739816469139/8d573c44-2da7-4f89-8fb7-4e15a0e75ba6.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>GetAstronauts Table -Astronauts Data</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739816666269/191c5c85-cbd0-47ef-858d-f654b74a2df3.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>ISSOrbit Table - ISS Trajectory in Past 90 minutes</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739816756114/79a4bac0-9830-416f-9c95-ec376eec2481.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-08-show-time-in-powerbi">Step 08 - Show Time in PowerBI</h3>
<p>I have enabled page-level refresh every 3 seconds in Power BI (Desktop/Service), ensuring that the latest location and details are updated every 3 seconds, providing a near real-time experience.</p>
<p><mark>At the time of writing this blog, the ISS is traveling across the Atlantic Ocean at an astonishing speed of 27,584 km/h.</mark></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739885640438/26c0578d-30b1-4cbe-9737-851db1fa670a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739885676428/24b8108a-a574-40b2-9f0d-581d048ea0a4.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>You may notice that I have included the <mark>altitude and speed</mark> (Km/H) of the ISS. To achieve this, I used an additional API (<a target="_blank" href="https://api.wheretheiss.at/v1/satellites/25544">https://api.wheretheiss.at/v1/satellites/25544</a>)<a target="_blank" href="https://api.wheretheiss.at/v1/satellites/25544">,</a> following the same process as above. The retrieved data was stored in a new table and integrated into the Power BI report.</p>
<ul>
<li><p><code>altitude</code> represents the ISS's altitude above Earth's surface in kilometers.</p>
</li>
<li><p><code>velocity</code>/speed indicates the ISS's speed in kilometers per hour.(See the speed 😮😮😮)</p>
</li>
</ul>
</blockquote>
<h2 id="heading-iss-live"><strong>ISS Live</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739890026455/01d05865-8594-4174-a9c2-85ca55381479.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-orbit-trajectory">Orbit Trajectory</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739890428744/c4f74cb2-c8e3-49b1-818f-7765724e9a9f.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This blog post showcased a real-time streaming solution for tracking the ISS and astronauts using Microsoft Fabric, KQL, and Power BI—all without relying on Azure Event Hub. The use of custom endpoints and Python/Pyspark notebooks makes this approach flexible and scalable.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>✅ <strong>No Need for Azure Logic Apps</strong> – This method simplifies the architecture while maintaining real-time streaming capabilities.<br />✅ <strong>Scheduled Refresh &amp; Auto Page Refresh</strong> – Notebooks are scheduled for refresh, and Power BI auto-refreshes every 3 seconds for a near real-time experience.<br />✅ <strong>Lightweight &amp; Efficient</strong> – Using Python notebooks, the resource usage remains minimal (max <strong>2vCores/16GB</strong>), avoiding the need for a heavy cluster.<br />✅ <strong>Fully Housed in Microsoft Fabric</strong> – The entire solution is built within <strong>Microsoft Fabric</strong>, leveraging <strong>Notebooks, Eventstream, KQL DB, and Power BI</strong> for a seamless experience.</p>
<p>🔗 <strong>Resources</strong> – Check out my <a target="_blank" href="https://github.com/nalakan/ISS-Demo">GitHub repo</a> for PBIX files, KQL queries, and other references.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Honestly, this has been the most time-consuming and effort-intensive blog post I've written so far. But it's all for a good purpose, and I'm genuinely happy about that!</div>
</div>

<p><strong>As usual Thanks for Reading !!!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Microsoft Fabric Real-Time Intelligence with Eventstream Custom Endpoints]]></title><description><![CDATA[Overview
Navigating the world of real-time data processing can be complex, but Microsoft Fabric's Real-Time Intelligence with Eventstream makes it surprisingly intuitive. This guide walks you through a practical scenario where we'll transform randomi...]]></description><link>https://bidiaries.com/microsoft-fabric-real-time-intelligence-with-eventstream-custom-endpoints</link><guid isPermaLink="true">https://bidiaries.com/microsoft-fabric-real-time-intelligence-with-eventstream-custom-endpoints</guid><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[fabric]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Fri, 07 Feb 2025 16:26:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/PIOgkhaF3WA/upload/2313dc648e8a14082403ef8f0aa235ea.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-overview">Overview</h2>
<p>Navigating the world of real-time data processing can be complex, but Microsoft Fabric's <a target="_blank" href="https://learn.microsoft.com/en-us/fabric/real-time-intelligence/">Real-Time Intelligence</a> with <mark>Eventstream</mark> makes it surprisingly intuitive. This guide walks you through a practical scenario where we'll transform randomized locally generated bogus humidity sensor data using custom endpoints as both <strong>input and output sources.</strong></p>
<h2 id="heading-system-architecture">System Architecture</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738930414278/8c8be26a-d984-4d09-886f-3fc06841453b.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Fabric Eventstream in this case dual acts as endpoints to receive and send data.</div>
</div>

<p>The architecture consists of the following stages:</p>
<ol>
<li><p><strong>Custom Endpoint as input Data Source:</strong> Sensor data is ingested via a local custom endpoint(Generated in the local system via VS Code)</p>
</li>
<li><p><strong>Microsoft Fabric Eventstream:</strong> The data flows through transformations to refine and enhance its usability.</p>
</li>
<li><p><strong>Derived Streams as output Data Source:</strong> Processed data is sent to another stream and acts as an endpoint to send data.</p>
</li>
<li><p><strong>Eventhouse KQL Database:</strong> Data is stored for further analytics and historical tracking.</p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><strong>Fabric Eventstream</strong> configured in <strong>Microsoft Fabric (Because we need to get the Eventhub Name and Primary Connection String to be included in the producer code)</strong></li>
</ul>
<h2 id="heading-step-1-simulating-humidity-sensor-data">Step 1: Simulating Humidity Sensor Data</h2>
<p>A producer script simulates sensor data and sends it to a custom endpoint. This will be executed in VS Code. As a best practice, the Event Hub name and primary connection strings should be stored securely, such as in Azure Key Vault, and should not be exposed. However, for demonstration purposes, I will explain them as they are.</p>
<pre><code class="lang-python"><span class="hljs-comment"># INSTALL azure-eventhub before running!</span>
<span class="hljs-comment"># pip command:</span>
<span class="hljs-comment"># pip install azure-eventhub</span>
<span class="hljs-comment"># Primary Stream</span>

<span class="hljs-keyword">from</span> azure.eventhub <span class="hljs-keyword">import</span> EventHubProducerClient, EventData
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime, timezone
<span class="hljs-keyword">import</span> hashlib
<span class="hljs-keyword">import</span> random
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> json

<span class="hljs-comment"># Replace the placeholders with your Event Hubs connection string and event hub name</span>
EVENTHUB_NAME = <span class="hljs-string">'es_d8ecd8c2-45c2-41e8-8f67-2d37e0c8112b'</span>
CONNECTION_STR = <span class="hljs-string">'Endpoint=sb:/'</span>
<span class="hljs-comment"># Configuration variables</span>
MIN_HUMIDITY = <span class="hljs-number">30.0</span>          <span class="hljs-comment"># Minimum humidity value</span>
MAX_HUMIDITY = <span class="hljs-number">60.0</span>          <span class="hljs-comment"># Maximum humidity value</span>
COUNTRY = <span class="hljs-string">"Canada"</span>           <span class="hljs-comment"># Country</span>
CITY = <span class="hljs-string">"Toronto"</span>             <span class="hljs-comment"># City</span>
SLEEP_TIME = <span class="hljs-number">5</span>               <span class="hljs-comment"># Sleep time before sending next event</span>

<span class="hljs-comment"># Example message</span>
<span class="hljs-string">'''
{
    "country": "Canada",
    "city": "Toronto",
    "timestamp": "2025-01-25T12:36:03.826769+00:00",
    "humidity_readings": [
        {
            "sensor": "sensor_1",
            "humidity": "45.34"
        },
        {
            "sensor": "sensor_2",
            "humidity": "50.21"
        },
        {
            "sensor": "sensor_3",
            "humidity": "55.69"
        }
    ],
    "event_id": "88709af29e138d8d906e009a800ebaddacd46d89d20ec91151e2ab91557170c5"
}
'''</span>

<span class="hljs-comment"># Create a producer client to send messages to the event hub</span>
producer = EventHubProducerClient.from_connection_string(conn_str=CONNECTION_STR, eventhub_name=EVENTHUB_NAME)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_fake_humidity</span>(<span class="hljs-params">min_humidity, max_humidity</span>):</span>
    <span class="hljs-string">"""Simulate a fake humidity reading within the specified range or generate null."""</span>
    <span class="hljs-keyword">return</span> str(round(random.uniform(min_humidity, max_humidity), <span class="hljs-number">2</span>))

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_random_sensor_readings</span>(<span class="hljs-params">min_humidity, max_humidity</span>):</span>
    <span class="hljs-string">"""Generate a list of humidity readings for random sensors."""</span>
    sensors = [<span class="hljs-string">"sensor_1"</span>, <span class="hljs-string">"sensor_2"</span>, <span class="hljs-string">"sensor_3"</span>]
    selected_sensors = random.sample(sensors, random.randint(<span class="hljs-number">1</span>, len(sensors)))
    <span class="hljs-keyword">return</span> [{<span class="hljs-string">"sensor"</span>: sensor, <span class="hljs-string">"humidity"</span>: generate_fake_humidity(min_humidity, max_humidity)}
            <span class="hljs-keyword">for</span> sensor <span class="hljs-keyword">in</span> selected_sensors]

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_event_id</span>(<span class="hljs-params">payload</span>):</span>
    <span class="hljs-string">"""Generate a SHA256 hash as a unique event ID."""</span>
    hash_object = hashlib.sha256(json.dumps(payload, sort_keys=<span class="hljs-literal">True</span>).encode(<span class="hljs-string">'utf-8'</span>))
    <span class="hljs-keyword">return</span> hash_object.hexdigest()

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_current_timestamp</span>():</span>
    <span class="hljs-string">"""Return the current timestamp in ISO 8601 format."""</span>
    <span class="hljs-keyword">return</span> datetime.now(timezone.utc).isoformat()

<span class="hljs-keyword">try</span>:
    <span class="hljs-comment"># Continuously generate and send fake humidity readings</span>
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># Create a batch.</span>
        event_data_batch = producer.create_batch()

        <span class="hljs-comment"># Generate random sensor readings</span>
        humidity_readings = get_random_sensor_readings(MIN_HUMIDITY, MAX_HUMIDITY)

        <span class="hljs-comment"># Create the payload</span>
        payload = {
            <span class="hljs-string">"country"</span>: COUNTRY,
            <span class="hljs-string">"city"</span>: CITY,
            <span class="hljs-string">"timestamp"</span>: get_current_timestamp(),
            <span class="hljs-string">"humidity_readings"</span>: humidity_readings
        }

        <span class="hljs-comment"># Generate an event_id</span>
        payload[<span class="hljs-string">"event_id"</span>] = generate_event_id(payload)

        <span class="hljs-comment"># Format the message as JSON</span>
        message = json.dumps(payload)

        <span class="hljs-comment"># Add the JSON-formatted message to the batch</span>
        event_data_batch.add(EventData(message))

        <span class="hljs-comment"># Send the batch of events to the event hub</span>
        producer.send_batch(event_data_batch)

        print(json.dumps(json.loads(message), indent=<span class="hljs-number">4</span>))
        print(event_data_batch)

        <span class="hljs-comment"># Wait for a bit before sending the next reading</span>
        time.sleep(SLEEP_TIME)
<span class="hljs-keyword">except</span> KeyboardInterrupt:
    print(<span class="hljs-string">"Stopped by the user"</span>)
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    print(<span class="hljs-string">f"Error: <span class="hljs-subst">{e}</span>"</span>)
<span class="hljs-keyword">finally</span>:
    <span class="hljs-comment"># Close the producer</span>
    producer.close()
</code></pre>
<blockquote>
<p>Here’s a brief breakdown of the code snippet:</p>
<ul>
<li><p>It generates random humidity readings between 30% and 60% for three sensors.</p>
</li>
<li><p>The data includes location (<code>Canada, Toronto</code>) and a timestamp.</p>
</li>
<li><p>The payload is serialized into JSON and assigned a unique event ID using SHA-256 hashing.</p>
</li>
<li><p>The script continuously generates and prints this data every 5 seconds, simulating real-time sensor readings.</p>
</li>
<li><p>It runs in an infinite loop until manually stopped (<code>KeyboardInterrupt</code>).</p>
</li>
<li><p>Event hub name and Connection String-Primary key should be obtained from folloiwng (That’s why we need to first create an Eventstream and then select the source as CustomEndpoint )</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942204045/02dccf4c-a0ae-4344-820d-0673ac84df7d.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</blockquote>
<p>Following are the payload output in VS Code</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738933296778/048e7714-04e0-4a4c-bae9-db0eac86f468.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942482132/543f62eb-a36a-49b7-a3dd-f7a7f091d1d0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-transforming-data-in-microsoft-fabric-eventstream">Step 2: Transforming Data in Microsoft Fabric Eventstream</h2>
<p>In these steps, i will explain different transformations done to the generated Eventstream output.</p>
<h3 id="heading-21-expanding-humidity-readings"><strong>2.1 Expanding Humidity Readings</strong></h3>
<p>This transformation expands the <code>humidity_readings</code> array into individual events using <mark>Expand</mark> transformation function,what the Expand function does is <mark>‘Create a new row for each value within an array’ </mark> like below.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"sensor"</span>: <span class="hljs-string">"sensor_1"</span>,
    <span class="hljs-attr">"humidity"</span>: <span class="hljs-number">45.5</span>,
    <span class="hljs-attr">"country"</span>: <span class="hljs-string">"Canada"</span>,
    <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-02-07T12:00:00Z"</span>,
    <span class="hljs-attr">"event_id"</span>: <span class="hljs-string">"&lt;unique_hash&gt;"</span>
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942783997/796918bd-c5b4-457b-8f4e-058263ffe9c8.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-22-managed-field-transformations"><strong>2.2 Managed Field Transformations</strong></h3>
<p>Renaming column names and data type changes are done in this step</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"sensor"</span>: <span class="hljs-string">"sensor_1"</span>,
    <span class="hljs-attr">"humidity"</span>: <span class="hljs-number">45.5</span>,
    <span class="hljs-attr">"country"</span>: <span class="hljs-string">"Canada"</span>,
    <span class="hljs-attr">"event_id"</span>: <span class="hljs-string">"&lt;unique_hash&gt;"</span>
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942877361/3b935c14-c35e-4202-a92b-e11e4a0db73f.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-23-group-by-transformation"><strong>2.3</strong> Group by Transformation</h3>
<p>We will add a new column based on the average humidity for each given Event_id. To achieve this, first, perform a group-by operation and then Left join the result with the original table stream.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942967090/1a204cd4-158f-420b-a724-234d58fb4f34.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-23-left-join-and-managed-fields-transformations"><strong>2.3</strong> Left Join and Managed Fields Transformations</h3>
<p>(New AVG_humidity column has been added)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738943003300/c3cc517f-86a9-4c68-bcf3-20dbb1fad543.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738943070140/8c55d7ec-2742-4e27-8715-0a9fa509d8c2.png" alt class="image--center mx-auto" /></p>
<p>These Processed data are then routed to:</p>
<ol>
<li><p><strong>A derived stream</strong> /<strong>secondary stream</strong> which acts as an input for another custom endpoint.</p>
</li>
<li><p><strong>An Eventhouse/KQL DB (Will not explain the usage of this as the use case in this post is custom endpoints)</strong></p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738943136285/1a0fefc4-bdea-4a0f-b6f8-83e4149cbc6a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-3-consuming-derived-stream-secondary-stream-via-custom-endpoint">Step 3: Consuming <strong>derived stream</strong> /<strong>secondary stream via custom endpoint</strong></h2>
<p>The following code block, when executed in VS Code, retrieves grouped and transformed data which pushes through the secondary stream. Similarly, a new EventHub name and connection string need to be added.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Secondary Stream</span>
<span class="hljs-comment">#  INSTALL azure-eventhub before running!</span>
<span class="hljs-comment"># pip install azure-eventhub</span>

<span class="hljs-keyword">from</span> azure.eventhub <span class="hljs-keyword">import</span> EventHubConsumerClient
<span class="hljs-keyword">import</span> json

<span class="hljs-comment"># Replace the placeholders with your Event Hubs connection string and event hub name</span>
EVENTHUB_NAME = <span class="hljs-string">'des_cee26d55-449b-4c4e-a0dc-ed481eaed80c'</span>
CONNECTION_STR = <span class="hljs-string">'Endpoint=sb://.windows.net/;'</span>
CONSUMER_GROUP = <span class="hljs-string">'$Default'</span>  <span class="hljs-comment"># Change if using a different consumer group</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_event</span>(<span class="hljs-params">partition_context, event</span>):</span>
    <span class="hljs-string">"""Callback function to process received events."""</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Decode event data</span>
        event_data = json.loads(event.body_as_str())

        <span class="hljs-comment"># Print received event</span>
        print(json.dumps(event_data, indent=<span class="hljs-number">4</span>))

        <span class="hljs-comment"># Update checkpoint</span>
        partition_context.update_checkpoint(event)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error processing event: <span class="hljs-subst">{e}</span>"</span>)

<span class="hljs-comment"># Create a consumer client</span>
consumer = EventHubConsumerClient.from_connection_string(
    conn_str=CONNECTION_STR,
    consumer_group=CONSUMER_GROUP,
    eventhub_name=EVENTHUB_NAME
)

<span class="hljs-keyword">try</span>:
    print(<span class="hljs-string">"Listening for events..."</span>)
    <span class="hljs-keyword">with</span> consumer:
        consumer.receive(
            on_event=on_event,
            starting_position=<span class="hljs-string">"-1"</span>  <span class="hljs-comment"># Read from the beginning</span>
        )
<span class="hljs-keyword">except</span> KeyboardInterrupt:
    print(<span class="hljs-string">"Stopped by user."</span>)
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    print(<span class="hljs-string">f"Error: <span class="hljs-subst">{e}</span>"</span>)
<span class="hljs-keyword">finally</span>:
    consumer.close()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738943260851/859f2d5f-d81f-4a16-ae13-d40258065168.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Adding <strong>custom endpoints</strong> as inputs and outputs in <strong>Microsoft Fabric Eventstream</strong> provides greater flexibility for real-time data ingestion and distribution, making it easier to integrate with various systems and applications.</p>
<h4 id="heading-as-an-input-source"><strong>As an Input Source</strong></h4>
<ul>
<li><p>Bring in real-time data from external systems, IoT devices, or third-party applications.</p>
</li>
<li><p>Connect to proprietary or industry-specific data sources that aren’t natively supported.</p>
</li>
<li><p>Ingest data from APIs, webhooks, or other event-driven sources.</p>
</li>
</ul>
<h4 id="heading-as-an-output-destination"><strong>As an Output Destination</strong></h4>
<ul>
<li><p>Route transformed event data to external applications, analytics platforms, or databases.</p>
</li>
<li><p>Send data to business systems, dashboards, or machine learning models for further processing.</p>
</li>
<li><p>Integrate with third-party APIs or messaging services for real-time automation.</p>
</li>
</ul>
<p>By using custom endpoints, you can seamlessly extend Eventstream’s capabilities to fit your specific needs, ensuring your data flows where it’s needed in real-time.</p>
<p><strong>Thanks for Reading !!!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Simplify Shortcuts Bulk Creation with PySpark + Semantic Link Labs in Microsoft Fabric]]></title><description><![CDATA[In today’s fast-evolving data landscape, managing shortcuts efficiently is critical for ensuring smooth access to your data, when you are working with Microsoft Fabric. As your data ecosystem grows, automating processes like MS Fabric Shortcuts creat...]]></description><link>https://bidiaries.com/simplify-shortcuts-bulk-creation-with-pyspark-semantic-link-labs-in-microsoft-fabric</link><guid isPermaLink="true">https://bidiaries.com/simplify-shortcuts-bulk-creation-with-pyspark-semantic-link-labs-in-microsoft-fabric</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[Shortcuts]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[semantic link labs]]></category><category><![CDATA[semantic-link]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Sun, 12 Jan 2025 06:51:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736648679387/8bd4db2e-07e5-44b0-821c-dec9feac8688.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>In today’s fast-evolving data landscape, managing shortcuts efficiently is critical for ensuring smooth access to your data, when you are working with Microsoft Fabric. As your data ecosystem grows, automating processes like MS Fabric <a target="_blank" href="https://learn.microsoft.com/en-us/fabric/onelake/onelake-shortcuts"><strong>Shortcuts</strong></a> creation becomes essential. This guide will explore how you can manage these tasks using <strong>Pyspark Notebooks and Semantic Link Labs.</strong></p>
<p>Whether you're moving shortcuts between environments or setting up shortcuts in bulk, you're in the right place. Let’s get started! 🚀</p>
<hr />
<h3 id="heading-shoutout-to-semantic-link-labs">Shoutout to Semantic Link Labs 🙌</h3>
<p>Before we dive into the scenarios, I want to give a big shoutout to <strong>Michael Kovalsky &amp; Contributing team</strong> for creating <strong>Semantic Link Labs</strong>. Among many other plethora of functions, this has made things much easier to manage. Thanks, Michael! 👏</p>
<blockquote>
<p>I'll skip the basics of what shortcuts are and it’s inner workings ,will jump straight to the point i.e. “Bulk creation of Shortcuts”. If you need a fundamental overview of Shortcuts, you can find it <a target="_blank" href="https://learn.microsoft.com/en-us/fabric/onelake/onelake-shortcuts">here</a>.</p>
</blockquote>
<hr />
<h3 id="heading-parameters-and-configuration"><strong>Parameters and Configuration</strong></h3>
<p>To ensure a smooth shortcut migration or bulk creation process, you'll need the following parameters:</p>
<ul>
<li><p><code>source_lakehouse</code>: The name of the source lakehouse (or environment) where the tables reside.</p>
</li>
<li><p><code>source_workspace</code>: The workspace associated with the source lakehouse (or environment).</p>
</li>
<li><p><code>destination_lakehouse</code>: The name of the destination lakehouse where shortcuts will be created.</p>
</li>
<li><p><code>destination_workspace</code>: The workspace associated with the destination.</p>
</li>
<li><p><code>prefix</code>: (Optional) A prefix to prepend to shortcut names for better organization and clarity.</p>
</li>
</ul>
<p>These parameters ensure the scripts know exactly where to retrieve the data from and where to create the shortcuts.</p>
<hr />
<h3 id="heading-why-shortcuts"><strong>Why Shortcuts?</strong> 🔄</h3>
<p>Shortcuts are an incredibly useful tool when managing data environments like lakehouses. They are essentially metadata pointers that provide access to tables without duplicating data. The advantages of using shortcuts include:</p>
<ul>
<li><p><strong>Data Sharing</strong>: Shortcuts allow you to share and access data across environments, whether it's lakehouses, workspaces, or other systems.</p>
</li>
<li><p><strong>Organizational Clarity</strong>: Shortcuts help you organize your tables better, making large datasets more manageable.</p>
</li>
<li><p><strong>Reducing Redundancy</strong>: They prevent unnecessary duplication of data, saving valuable storage resources.</p>
</li>
</ul>
<p>By efficiently managing shortcuts, you can significantly streamline your workflows, especially in complex or large-scale migrations.</p>
<hr />
<h3 id="heading-scenario-1-automated-shortcut-migration-with-validation"><strong>Scenario 1 : Automated Shortcut Migration with Validation</strong> ✅</h3>
<h4 id="heading-overview"><strong>Overview</strong></h4>
<p>In this scenario, we're migrating shortcuts from a source environment (lakehouse/workspace) to a destination one. It involves making sure that:</p>
<ul>
<li><p><strong>No Duplicate Shortcuts</strong>: Shortcuts that already exist in the destination are not recreated.</p>
</li>
<li><p><strong>Naming Conventions</strong>: You can optionally prepend or append prefixes or suffixes to ensure naming consistency.</p>
</li>
<li><p><strong>Error Handling</strong>: The script gracefully handles any errors, ensuring the migration process doesn’t fail if something goes wrong.</p>
</li>
</ul>
<p>In my case, the tables in my source lakehouse had different prefixes, which required me to modify them with a different prefix or suffix when creating the shortcuts in the destination. This ensures consistency across different environments.</p>
<h4 id="heading-semantic-link-labs-functions-used"><strong>Semantic Link Labs Functions Used</strong>:</h4>
<ul>
<li><p><code>sempy_labs.list_shortcuts</code></p>
</li>
<li><p><code>sempy_labs.lakehouse.create_shortcut_onelake</code></p>
</li>
<li><p><em>Other functions and code snippets are mainly to handle the failures and code sanitization.</em></p>
</li>
</ul>
<h4 id="heading-code-implementation-first-install-semantic-link-labs-pip-install-semantic-link-labs"><strong>Code Implementation</strong> (First install Semantic Link Labs - %pip install semantic-link-labs)</h4>
<pre><code class="lang-python"> <span class="hljs-comment"># Author Nalaka</span>
<span class="hljs-keyword">import</span> sempy_labs
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Dict, Tuple
<span class="hljs-keyword">import</span> time

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">safe_api_call</span>(<span class="hljs-params">func, retries=<span class="hljs-number">3</span>, delay=<span class="hljs-number">2</span>, **kwargs</span>):</span>
    <span class="hljs-string">"""
    Safely calls a function with retry logic.
    """</span>
    <span class="hljs-keyword">for</span> attempt <span class="hljs-keyword">in</span> range(retries):
        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">return</span> func(**kwargs)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Attempt <span class="hljs-subst">{attempt + <span class="hljs-number">1</span>}</span> failed: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">if</span> attempt &lt; retries - <span class="hljs-number">1</span>:
                time.sleep(delay)
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">raise</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_shortcuts_batch</span>(<span class="hljs-params">
    source_lakehouse: str,
    source_workspace: str,
    destination_lakehouse: str,
    destination_workspace: str,
    prefix: str = <span class="hljs-string">""</span>
</span>) -&gt; Dict[str, Tuple[bool, str]]:</span>
    <span class="hljs-string">"""
    Create shortcuts automatically, ensuring the correct table name is used from the OneLake Path column.
    """</span>
    results = {}

    print(<span class="hljs-string">"Fetching shortcuts from source workspace..."</span>)
    shortcuts_df = safe_api_call(
        sempy_labs.list_shortcuts,
        lakehouse=source_lakehouse,
        workspace=source_workspace
    )

    <span class="hljs-comment"># Ensuring necessary columns</span>
    <span class="hljs-keyword">if</span> shortcuts_df.empty <span class="hljs-keyword">or</span> <span class="hljs-string">'Shortcut Name'</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> shortcuts_df.columns <span class="hljs-keyword">or</span> <span class="hljs-string">'OneLake Path'</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> shortcuts_df.columns:
        print(<span class="hljs-string">"Missing required data columns ('Shortcut Name' or 'OneLake Path')."</span>)
        <span class="hljs-keyword">return</span> results

    <span class="hljs-comment"># Fetch existing shortcuts in the destination workspace</span>
    print(<span class="hljs-string">"Fetching existing shortcuts from destination workspace..."</span>)
    destination_shortcuts_df = safe_api_call(
        sempy_labs.list_shortcuts,
        lakehouse=destination_lakehouse,
        workspace=destination_workspace
    )
    existing_shortcuts = set(destination_shortcuts_df[<span class="hljs-string">'Shortcut Name'</span>]) <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> destination_shortcuts_df.empty <span class="hljs-keyword">else</span> set()

    <span class="hljs-comment"># Iterate through shortcut names</span>
    print(<span class="hljs-string">"Processing shortcuts..."</span>)
    <span class="hljs-keyword">for</span> _, row <span class="hljs-keyword">in</span> shortcuts_df.iterrows():
        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Extract actual table name from OneLake Path (removing 'Tables/' prefix)</span>
            actual_table_name = row[<span class="hljs-string">'OneLake Path'</span>].removeprefix(<span class="hljs-string">'Tables/'</span>) <span class="hljs-comment">#Change accordingly</span>

            <span class="hljs-comment"># Prepare shortcut name with optional prefix</span>
            new_shortcut_name = <span class="hljs-string">f"<span class="hljs-subst">{prefix}</span><span class="hljs-subst">{row[<span class="hljs-string">'Shortcut Name'</span>]}</span>"</span>

            <span class="hljs-comment"># Skip if the shortcut already exists</span>
            <span class="hljs-keyword">if</span> new_shortcut_name <span class="hljs-keyword">in</span> existing_shortcuts:
                results[row[<span class="hljs-string">'Shortcut Name'</span>]] = (<span class="hljs-literal">True</span>, <span class="hljs-string">f"Shortcut '<span class="hljs-subst">{new_shortcut_name}</span>' already exists. Skipped."</span>)
                <span class="hljs-keyword">continue</span>

            <span class="hljs-comment"># Create shortcut using details from the row</span>
            safe_api_call(
                sempy_labs.lakehouse.create_shortcut_onelake,
                table_name=actual_table_name, 
                source_lakehouse=row[<span class="hljs-string">'Source Item Name'</span>],  <span class="hljs-comment"># Source lakehouse from the row</span>
                source_workspace=row[<span class="hljs-string">'Source Workspace Name'</span>],  <span class="hljs-comment"># Source workspace from the row</span>
                destination_lakehouse=destination_lakehouse,
                destination_workspace=destination_workspace,
                shortcut_name=new_shortcut_name
            )

            success_msg = (
                <span class="hljs-string">f"Successfully created shortcut: <span class="hljs-subst">{new_shortcut_name}</span>\n"</span>
                <span class="hljs-string">f"Source: <span class="hljs-subst">{row[<span class="hljs-string">'Source Workspace Name'</span>]}</span>/<span class="hljs-subst">{row[<span class="hljs-string">'Source Item Name'</span>]}</span>\n"</span>
                <span class="hljs-string">f"Destination: <span class="hljs-subst">{destination_workspace}</span>/<span class="hljs-subst">{destination_lakehouse}</span>"</span>
            )
            results[row[<span class="hljs-string">'Shortcut Name'</span>]] = (<span class="hljs-literal">True</span>, success_msg)

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-comment"># Log the error with detailed information</span>
            error_details = <span class="hljs-string">f"Shortcut: <span class="hljs-subst">{row[<span class="hljs-string">'Shortcut Name'</span>]}</span>\n"</span> \
                            <span class="hljs-string">f"Source Lakehouse: <span class="hljs-subst">{row[<span class="hljs-string">'Source Item Name'</span>]}</span>\n"</span> \
                            <span class="hljs-string">f"Source Workspace: <span class="hljs-subst">{row[<span class="hljs-string">'Source Workspace Name'</span>]}</span>\n"</span> \
                            <span class="hljs-string">f"Error: <span class="hljs-subst">{str(e)}</span>"</span>
            results[row[<span class="hljs-string">'Shortcut Name'</span>]] = (<span class="hljs-literal">False</span>, error_details)
            print(<span class="hljs-string">f"Failed to process: <span class="hljs-subst">{error_details}</span>"</span>)

    <span class="hljs-keyword">return</span> results

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_results_summary</span>(<span class="hljs-params">results: Dict[str, Tuple[bool, str]]</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-string">"""
    Print detailed summary of shortcut creation results.
    """</span>
    print(<span class="hljs-string">"\n=== EXECUTION SUMMARY ==="</span>)
    print(<span class="hljs-string">f"Total shortcuts processed: <span class="hljs-subst">{len(results)}</span>"</span>)

    successful = sum(<span class="hljs-number">1</span> <span class="hljs-keyword">for</span> status, _ <span class="hljs-keyword">in</span> results.values() <span class="hljs-keyword">if</span> status)
    failed = len(results) - successful

    print(<span class="hljs-string">f"Successful: <span class="hljs-subst">{successful}</span>"</span>)
    print(<span class="hljs-string">f"Failed: <span class="hljs-subst">{failed}</span>"</span>)

    <span class="hljs-keyword">if</span> failed &gt; <span class="hljs-number">0</span>:
        print(<span class="hljs-string">"\n=== FAILED OPERATIONS ==="</span>)
        <span class="hljs-keyword">for</span> table, (status, message) <span class="hljs-keyword">in</span> results.items():
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> status:
                print(<span class="hljs-string">f"\nTable: <span class="hljs-subst">{table}</span>"</span>)
                print(message)

    <span class="hljs-keyword">if</span> successful &gt; <span class="hljs-number">0</span>:
        print(<span class="hljs-string">"\n=== SUCCESSFUL OPERATIONS ==="</span>)
        <span class="hljs-keyword">for</span> table, (status, message) <span class="hljs-keyword">in</span> results.items():
            <span class="hljs-keyword">if</span> status:
                print(<span class="hljs-string">f"\nTable: <span class="hljs-subst">{table}</span>"</span>)
                print(message)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    config = {
        <span class="hljs-comment"># Source location to list shortcuts</span>
        <span class="hljs-string">"source_lakehouse"</span>: <span class="hljs-string">"SRC_LH_00"</span>,
        <span class="hljs-string">"source_workspace"</span>: <span class="hljs-string">"SRC_WS_00"</span>,

        <span class="hljs-comment"># Destination location for new shortcuts</span>
        <span class="hljs-string">"destination_lakehouse"</span>: <span class="hljs-string">"TGT_LH_00"</span>,
        <span class="hljs-string">"destination_workspace"</span>: <span class="hljs-string">"TGT_WS_00"</span>,

        <span class="hljs-string">"prefix"</span>: <span class="hljs-string">""</span>  <span class="hljs-comment"># Optional prefix for destination shortcut name</span>
    }

    <span class="hljs-comment"># Measure execution time</span>
    start_time = time.time()

    print(<span class="hljs-string">"Starting shortcut creation process..."</span>)
    results = create_shortcuts_batch(**config)

    end_time = time.time()
    print_results_summary(results)

    print(<span class="hljs-string">f"\nExecution time: <span class="hljs-subst">{end_time - start_time:<span class="hljs-number">.2</span>f}</span> seconds"</span>)
</code></pre>
<h4 id="heading-explanation"><strong>Explanation</strong></h4>
<p>This script helps:</p>
<ul>
<li><p><strong>Fetch Shortcuts</strong>: Retrieves the shortcuts from the source and checks for any that already exist in the destination.</p>
</li>
<li><p><strong>Validation and Renaming</strong>: It ensures that no duplicates are created, with the option to add prefixes for easy organization. (Removed prefix named “Tables”)</p>
</li>
<li><p><strong>Error Handling</strong>: Uses retry logic with the <code>safe_api_call</code> function to handle temporary issues, ensuring smooth migration.</p>
</li>
<li><p><strong>Create Shortcuts</strong>: Finally, it loops through the list of source shortcuts and creates them at the destination.</p>
</li>
</ul>
<h4 id="heading-use-case"><strong>Use Case</strong></h4>
<ul>
<li><p>Ideal for migrating shortcuts between environments (e.g., from Dev/Test to production).</p>
</li>
<li><p>Helpful when maintaining naming consistency during the migration process.</p>
</li>
</ul>
<h4 id="heading-what-does-this-scenario-solve"><strong>What Does This Scenario Solve?</strong></h4>
<p><mark>This process is perfect for environments where shortcuts need to be moved between different workspaces while maintaining data integrity and ensuring that shortcuts are not duplicated. </mark> It ensures that:</p>
<ul>
<li><p>The right shortcuts are created with the right names.</p>
</li>
<li><p>Any existing shortcuts that meet the same criteria are not duplicated.</p>
</li>
<li><p>Errors are handled through retry mechanisms to prevent interruptions.</p>
</li>
</ul>
<p>It’s a flexible way to migrate shortcuts without unnecessary manual intervention.</p>
<p><strong>Sample output of the above code</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736621058120/8ea6cea3-9772-454f-8829-7542184d0578.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736621130330/7b25eaa6-eb2b-4d49-be95-82f8879eb4a4.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736621176559/1278ef1f-6bc1-4b01-83c3-154471685892.png" alt /></p>
<hr />
<h3 id="heading-scenario-2-bulk-shortcut-creation-from-existing-source-tables"><strong>Scenario 2 : Bulk Shortcut Creation</strong> from existing source tables🏋️‍♂️</h3>
<h4 id="heading-overview-1"><strong>Overview</strong></h4>
<p>Bulk shortcut creation allows you to create shortcuts for all tables in a source environment in one go, with the option to replace existing shortcuts if necessary. This is perfect for situations where you're setting up a new environment or refreshing shortcuts to reflect updated data sources.</p>
<p>If you don’t want to check and delete existing shortcuts, you can skip the code blocks that handle the deletion of shortcuts. This step is optional and can be bypassed if the existing shortcuts aren’t a concern for your scenario.</p>
<h4 id="heading-semantic-link-labs-functions-used-1"><strong>Semantic Link Labs Functions Used</strong>:</h4>
<ul>
<li><p><code>sempy_labs.lakehouse.get_lakehouse_tables</code></p>
</li>
<li><p><code>sempy_labs.lakehouse.delete_shortcut</code></p>
</li>
<li><p><code>sempy_labs.lakehouse.create_shortcut_onelake</code></p>
</li>
</ul>
<h4 id="heading-code-implementation-first-install-semantic-link-labs-pip-install-semantic-link-labs-1"><strong>Code Implementation</strong> (First install Semantic Link Labs - %pip install semantic-link-labs)</h4>
<pre><code class="lang-python"><span class="hljs-comment">#Author Nalaka</span>
<span class="hljs-keyword">import</span> sempy_labs

<span class="hljs-comment"># Parameters for source and destination</span>
source_lakehouse = <span class="hljs-string">"SRC_LH_01"</span>
source_workspace = <span class="hljs-string">"SRC_WS_01"</span>
destination_lakehouse = <span class="hljs-string">"TGT_LH_01"</span>
destination_workspace = <span class="hljs-string">"TGT_WS_01"</span>

<span class="hljs-comment"># Fetch the list of tables from the source lakehouse</span>
tables = sempy_labs.lakehouse.get_lakehouse_tables(
    lakehouse=source_lakehouse,
    workspace=source_workspace
)

<span class="hljs-comment"># Function to check for existing shortcuts</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">shortcut_exists</span>(<span class="hljs-params">destination_lakehouse, destination_workspace, shortcut_name</span>):</span>
    existing_shortcuts = sempy_labs.lakehouse.get_lakehouse_tables(
        lakehouse=destination_lakehouse,
        workspace=destination_workspace
    )
    <span class="hljs-keyword">return</span> shortcut_name <span class="hljs-keyword">in</span> existing_shortcuts[<span class="hljs-string">'Table Name'</span>].values

<span class="hljs-comment"># Iterate through each row in the DataFrame to process the 'Table Name'</span>
<span class="hljs-keyword">for</span> _, row <span class="hljs-keyword">in</span> tables.iterrows():
    shortcut_name = row[<span class="hljs-string">'Table Name'</span>]  <span class="hljs-comment"># Extract 'Table Name' column as shortcut name</span>
    print(<span class="hljs-string">f"Processing shortcut: '<span class="hljs-subst">{shortcut_name}</span>'"</span>)

    <span class="hljs-comment"># Check if the shortcut already exists</span>
    <span class="hljs-keyword">if</span> shortcut_exists(destination_lakehouse, destination_workspace, shortcut_name):
        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Delete the existing shortcut</span>
            sempy_labs.lakehouse.delete_shortcut(
                shortcut_name=shortcut_name,
                lakehouse=destination_lakehouse,
                workspace=destination_workspace
            )
            print(<span class="hljs-string">f"🟢 The shortcut '<span class="hljs-subst">{shortcut_name}</span>' in '<span class="hljs-subst">{destination_workspace}</span>' has been deleted."</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Failed to delete existing shortcut '<span class="hljs-subst">{shortcut_name}</span>'. Error: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">continue</span>  <span class="hljs-comment"># Skip to the next shortcut if delete fails</span>

    <span class="hljs-comment"># Create a new shortcut</span>
    <span class="hljs-keyword">try</span>:
        sempy_labs.lakehouse.create_shortcut_onelake(
            table_name=shortcut_name,
            source_lakehouse=source_lakehouse,
            source_workspace=source_workspace,
            destination_lakehouse=destination_lakehouse,
            destination_workspace=destination_workspace
        )
        print(<span class="hljs-string">f"🟢 Shortcut created successfully for: '<span class="hljs-subst">{shortcut_name}</span>'"</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"🔴 Failed to create shortcut for '<span class="hljs-subst">{shortcut_name}</span>'. Error: <span class="hljs-subst">{e}</span>"</span>)

print(<span class="hljs-string">"All shortcuts processed successfully."</span>)
</code></pre>
<p>If you don't want to check for and delete existing shortcuts, you can skip <strong>Code Block 3</strong> (shortcut existence check) and <strong>Code Block 4</strong> (shortcut deletion).</p>
<h4 id="heading-what-does-this-code-block-do"><strong>What Does This Code Block Do?</strong></h4>
<ul>
<li><p><strong>List Tables</strong>: Fetches all table names from the source lakehouse.</p>
</li>
<li><p><strong>Check Existing Shortcuts</strong>: Verifies whether shortcuts already exist in the destination.</p>
</li>
<li><p><strong>Delete &amp; Replace</strong>: Deletes existing shortcuts in the destination and recreates them.</p>
</li>
<li><p><strong>Log Progress</strong>: Prints status messages for each shortcut processed.</p>
</li>
</ul>
<h4 id="heading-what-does-this-scenario-solve-1"><strong>What Does This Scenario Solve?</strong></h4>
<p>This scenario simplifies the creation of shortcuts when you're dealing with:</p>
<ul>
<li><p><strong>Initial Setup</strong>: <mark>If you're setting up a new environment and need to create shortcuts for all tables, this method will automate that process.</mark></p>
</li>
<li><p><strong>Shortcut Refresh</strong>: If your data source has been updated, this process will replace the old shortcuts with new ones, ensuring the shortcuts are always accurate.</p>
</li>
</ul>
<p>It’s a straightforward, automated way to ensure your shortcuts are set up or refreshed efficiently.</p>
<p><em>Sample output of the above code</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736621538981/cc94a860-d6a3-4918-b99f-7d8db0937fb5.png" alt /></p>
<hr />
<h3 id="heading-finishing-touches"><strong>Finishing Touches</strong> ✨</h3>
<p>Managing shortcut creation and migration can greatly enhance your data operations, making them more structured and efficient. With the help of the <strong>Semantic Link Labs</strong>, you can automate these processes, significantly reducing manual effort and minimizing errors. Whether you're migrating shortcuts between environments or creating them in bulk, the methods and strategies discussed here will help you streamline your workflows and ensure your data stays organized.</p>
<p>Happy shortcutting ! Thanks for Reading !! 👍</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[YouTube Creations :]]></title><description><![CDATA[Microsoft Fabric Capacity Management & Billing
https://www.youtube.com/watch?v=r-riS8HMKxE
 
🎙️ This podcast dives into how Fabric capacity works, including the evolution of bursting & smoothing, the differences between RI vs PAYG, and the billing i...]]></description><link>https://bidiaries.com/microsoft-fabric-capacity-management-and-billing</link><guid isPermaLink="true">https://bidiaries.com/microsoft-fabric-capacity-management-and-billing</guid><category><![CDATA[fabric]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[CapacityPlanning]]></category><category><![CDATA[Azure]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[dataengineering]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 31 Dec 2024 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/HywcR2JY_ks/upload/348a23c22a3ccfef44d36d2b560e6982.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-microsoft-fabric-capacity-management-amp-billing">Microsoft Fabric Capacity Management &amp; Billing</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=r-riS8HMKxE">https://www.youtube.com/watch?v=r-riS8HMKxE</a></div>
<p> </p>
<p>🎙️ This podcast dives into how Fabric capacity works, including the evolution of bursting &amp; smoothing, the differences between RI vs PAYG, and the billing impact of pausing, restarting, scaling up/down Fabric capacities. 💼✨ 🧾⚙️ Several helpful resources were used, including LinkedIn Fabric Billing Article Series by Matthew Farrow from Microsoft. 🚄 Feel free to adjust the playback speed to suit your preference.</p>
<hr />
<h1 id="heading-whats-new-in-microsoft-fabric-direct-lake-april-2025-onwards">What's New in Microsoft Fabric Direct Lake (April 2025 Onwards)</h1>
<p>Direct Lake on OneLake is a game-changer for working with large-scale data in OneLake, offering faster, simpler, and more flexible semantic modeling in Power BI. What is Direct Lake on OneLake? It's a new storage mode for Power BI semantic models that allows direct connection to Delta tables stored in Microsoft Fabric's OneLake. This bypasses the SQL analytics endpoint of Lakehouses and Warehouses.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=RB9C_X6KWA8&amp;t=5s">https://www.youtube.com/watch?v=RB9C_X6KWA8&amp;t=5s</a></div>
]]></content:encoded></item><item><title><![CDATA[Speaking Engagements]]></title><description><![CDATA[Public sessions

Real-Time Intelligence with Microsoft Fabric: Tracking the International Space Station
  Link :

https://www.youtube.com/watch?v=K09Z1PBf8YY
 

Journey through Incremental Refresh & Hybrid Tables in PowerBI.
  Link :
  %[https://yout...]]></description><link>https://bidiaries.com/speaking-engagements</link><guid isPermaLink="true">https://bidiaries.com/speaking-engagements</guid><category><![CDATA[speaking ]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[conference sessions]]></category><category><![CDATA[webinars]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Mon, 30 Dec 2024 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/QrqeusbpFMM/upload/9bb985cfa24a66eec32e653e47e0682f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-public-sessions"><em>Public sessions</em></h3>
<ul>
<li><p>Real-Time Intelligence with Microsoft Fabric: Tracking the International Space Station</p>
<p>  Link :</p>
</li>
<li><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=K09Z1PBf8YY">https://www.youtube.com/watch?v=K09Z1PBf8YY</a></div>
<p> </p>
</li>
<li><p>Journey through Incremental Refresh &amp; Hybrid Tables in PowerBI.</p>
<p>  Link :</p>
<p>  %[https://youtu.be/aA99-oVZPFk] </p>
</li>
</ul>
<hr />
<h3 id="heading-in-house-sessions">In-House Sessions</h3>
<p>Conducted brainstorming and knowledge-sharing sessions during "Wiley(Ex Employer) Innovation Week" and "Wiley Tech Talks".</p>
<p><mark>Consistently secured the </mark> <strong><mark>First place</mark></strong> <mark>with numerous upvotes for all Innovation Week </mark> <strong><mark>Speaking sessions</mark></strong> <mark>as listed below.</mark></p>
<ul>
<li><p>June 2022: Loading Datasets to Power BI Using Partitions</p>
</li>
<li><p>November 2022: PowerBI Perspectives</p>
</li>
<li><p>March 2023: ChatGPT Integration with PowerBI for Better Business Value</p>
</li>
<li><p>March 2023: Deeper Dive on ChatGPT and Power BI</p>
</li>
<li><p>May 2023: PowerBI Version Control for Humans</p>
</li>
<li><p>July 2023: Microsoft Fabric Deep Dive</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Power BI Deployment Pipelines Pitfalls / Lessons Learnt]]></title><description><![CDATA[Let's assume we are promoting an artifact (e.g., a semantic model) from the Dev environment to the Test environment using Deployment pipelines in PowerBI/Fabric.

1. 📦 Artifact Comparison vs Runtime Behavior

Pipelines compare model files (datasets/...]]></description><link>https://bidiaries.com/power-bi-deployment-pipelines-pitfalls-lessons-learnt</link><guid isPermaLink="true">https://bidiaries.com/power-bi-deployment-pipelines-pitfalls-lessons-learnt</guid><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[fabric]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Azure]]></category><category><![CDATA[#DataModelling]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Mon, 30 Dec 2024 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Rm3nWQiDTzg/upload/b9b4f30830efd094fbf5203a4e090613.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's assume we are promoting an artifact (e.g., a semantic model) from the Dev environment to the Test environment using Deployment pipelines in PowerBI/Fabric.</p>
<hr />
<h3 id="heading-1-artifact-comparison-vs-runtime-behavior">1. 📦 Artifact Comparison vs Runtime Behavior</h3>
<ul>
<li><p>Pipelines <strong>compare model files</strong> (datasets/reports) — <em>structure, not runtime behavior</em>.</p>
</li>
<li><p><strong>Connection settings at runtime</strong> (e.g., database server) are <strong>NOT</strong> part of artifact comparison.</p>
</li>
<li><p><strong>"No difference"</strong> means <em>file is same</em>, <strong>not</strong> that <em>connection string is right</em>!</p>
</li>
</ul>
<h3 id="heading-2-deployment-rules-behavior">2. 🚀 Deployment Rules Behavior</h3>
<ul>
<li><p><strong>Deployment rules are only applied during promotion.</strong></p>
</li>
<li><p><strong>Saving deployment rules</strong> does <strong>NOT</strong> change already deployed artifacts.</p>
</li>
<li><p><strong>You must promote again</strong> after setting deployment rules to apply them!</p>
</li>
</ul>
<h3 id="heading-3-how-differences-appear">3. ⚡ How Differences Appear</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Action</td><td>Pipeline Comparison</td></tr>
</thead>
<tbody>
<tr>
<td>Promote cleanly without manual changes</td><td>Same ✅</td></tr>
<tr>
<td>Manually change deployment rules after promote</td><td>Different ❗</td></tr>
<tr>
<td>Promote again with correct deployment rules</td><td>Same ✅</td></tr>
</tbody>
</table>
</div><h3 id="heading-4-environment-drift-trap">4. 🔥 Environment Drift Trap</h3>
<ul>
<li><p>If you manually change rules after promotion ➔ <strong>Environment drift detected</strong> ➔ Pipeline shows <strong>Different</strong>.</p>
</li>
<li><p>If rules applied during promotion ➔ <strong>No drift</strong> ➔ Pipeline shows <strong>Same</strong>.</p>
</li>
</ul>
<h3 id="heading-5-key-behavior">5. 🎯 Key Behavior</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Question</td><td>Answer</td></tr>
</thead>
<tbody>
<tr>
<td>Does saving deployment rule immediately update model?</td><td>❌ No</td></tr>
<tr>
<td>Does promoting apply deployment rules?</td><td>✅ Yes</td></tr>
<tr>
<td>Does "no difference" guarantee connection strings are correct?</td><td>❌ No</td></tr>
<tr>
<td>How to really guarantee correct server?</td><td>✅ Check parameters manually in Service + use proper deployment rules</td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-quick-pitfalls-to-remember">🧠 Quick Pitfalls to Remember</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Pitfall</td><td>How to Avoid</td></tr>
</thead>
<tbody>
<tr>
<td>Thinking "no difference" = correct server connection</td><td>Always manually check connection</td></tr>
<tr>
<td>Saving deployment rules but forgetting to promote again</td><td>After saving rules, <strong>always promote</strong> once more</td></tr>
<tr>
<td>Manually changing test/prod environment without re-promoting</td><td>Prefer controlled promotions with automatic rule apply</td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-steps">Steps</h1>
<pre><code class="lang-plaintext">pgsqlCopyEdit[ Dev Model ]
     |
     | (Promote)
     ↓
[ Test Model (initially same as Dev) ]
     |
     | (Edit Deployment Rules? Save? Must Promote Again!)
     ↓
[ Test Model (now runtime behavior changed to Test server) ]
</code></pre>
<p>✅ <strong>Artifact stays same inside.</strong><br />✅ <strong>Runtime settings change when promoting with deployment rules.</strong></p>
<hr />
<h1 id="heading-super-important">✅ Super Important :</h1>
<blockquote>
<p><strong>Promotion applies rules. Saving rules alone does nothing. "No difference" checks files, not servers. Manual changes cause drift. Always check connections!</strong></p>
</blockquote>
<p>Thanks for Reading ..</p>
]]></content:encoded></item><item><title><![CDATA[Portfolio-Some work samples]]></title><description><![CDATA[Marketing Analytics Dashboard

https://app.powerbi.com/links/BmPRvBLLKh?ctid=eb5fac66-36d7-4571-b8ca-f7ee9578d45c&pbi_source=linkShare

💡
Click on the images to enlarge them.






Lead Generation Dashboard

Data source -Dynamics 365


💡
Click on t...]]></description><link>https://bidiaries.com/portfolio-some-work-samples</link><guid isPermaLink="true">https://bidiaries.com/portfolio-some-work-samples</guid><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Mon, 30 Dec 2024 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ykgLX_CwtDw/upload/b0721eb3b7a49839fbeebf4150ca73c5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-marketing-analytics-dashboard">Marketing Analytics Dashboard</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756191840214/1ca580d8-5e53-4aa1-8898-5a9ab4f36bc0.png" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://app.powerbi.com/links/BmPRvBLLKh?ctid=eb5fac66-36d7-4571-b8ca-f7ee9578d45c&amp;pbi_source=linkShare">https://app.powerbi.com/links/BmPRvBLLKh?ctid=eb5fac66-36d7-4571-b8ca-f7ee9578d45c&amp;pbi_source=linkShare</a></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Click on the images to enlarge them.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756192015642/148ee8ab-aff2-427c-bedf-bd6ae67bf200.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756192095606/96752973-d178-4ab1-b0cd-a5c6ed036763.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756192155883/9a8519bf-93be-4a8c-ab36-777d9adf7dcd.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756192261569/fdd1953e-7b04-4927-b9e0-4b2994ae1c3c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-lead-generation-dashboard">Lead Generation Dashboard</h2>
<blockquote>
<p>Data source -Dynamics 365</p>
</blockquote>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Click on the images to enlarge them.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756193465691/fe3a8734-7e89-4e4f-bd15-8fc45d7d2114.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756193661370/6e52752e-ef55-4ed9-be35-8c6b692686fe.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756193866041/8e1a43e8-2276-4243-838d-819daf332a6b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756194144987/d4ed3e08-57ba-4de5-a640-7f68d0d17ccc.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-multi-slicer-panel-implementation">Multi Slicer Panel Implementation</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756195324895/72aa5b99-7059-421d-ac0a-36b548c3ee43.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-r-amp-d-nike-store-interactive-link">R &amp; D -Nike store (Interactive link)</h2>
<iframe width="800" height="500" src="https://app.powerbi.com/view?r=eyJrIjoiMTBlZTVmMWQtODlmNi00YTQ1LWE5YTQtOGI2MWVkOTViOGE1IiwidCI6ImViNWZhYzY2LTM2ZDctNDU3MS1iOGNhLWY3ZWU5NTc4ZDQ1YyIsImMiOjEwfQ%3D%3D"></iframe>]]></content:encoded></item><item><title><![CDATA[Deciphering Data Architectures: Closer Look at Different Paradigms]]></title><description><![CDATA[Navigating the intricate world of data architecture can feel overwhelming — but it doesn’t have to be. Let’s break down key concepts like relational data warehouses, data lakes, modern data warehouses, data fabric, data lakehouses, and most important...]]></description><link>https://bidiaries.com/deciphering-data-architectures-closer-look-at-different-paradigms</link><guid isPermaLink="true">https://bidiaries.com/deciphering-data-architectures-closer-look-at-different-paradigms</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Data Architecture]]></category><category><![CDATA[Data Mesh]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Databases]]></category><category><![CDATA[data structures]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Mon, 23 Dec 2024 15:48:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/v-3ujvHWuz0/upload/ca09ce1ad1e372b8d613388bc34404e4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Navigating the intricate world of data architecture can feel overwhelming — but it doesn’t have to be. Let’s break down key concepts like relational data warehouses, data lakes, modern data warehouses, data fabric, data lakehouses, and most importantly, data mesh. Along the way, we’ll uncover their roles, challenges, and best use cases.</p>
<blockquote>
<p>This blog post draws insights from <a target="_blank" href="https://www.amazon.com/Deciphering-Data-Architectures-Warehouse-Lakehouse/dp/1098150767"><mark>James Serra’s book, </mark> <em><mark>Deciphering Data Architectures</mark></em></a> and reflects his perspectives on the various data architectures discussed.</p>
</blockquote>
<hr />
<h3 id="heading-relational-data-warehouse-rdw">Relational Data Warehouse (RDW) 📊</h3>
<p><strong>What is it?</strong></p>
<p>A relational data warehouse serves as a centralized hub for consolidating data from multiple sources. It’s designed for historical analysis and provides the “single version of truth.” Unlike operational databases, it’s not intended for transactional (OLTP) purposes.</p>
<p><strong>Why use it?</strong></p>
<ul>
<li><p>Consolidates data for unified insights.<mark>(schema-on-write)</mark></p>
</li>
<li><p>Reduces the load on production systems.</p>
</li>
<li><p>Provides reliable historical trend analysis.</p>
</li>
<li><p>Ensures enhanced security and data quality.</p>
</li>
</ul>
<p><strong>🔍 Questions to consider:</strong></p>
<ul>
<li><p>Does your organization depend on historical reporting?</p>
</li>
<li><p>Are production systems overburdened with analytical queries?</p>
</li>
</ul>
<blockquote>
<p>RDWs have both a compute engine and storage. The compute engine is the processing power used to query the data. The storage is relational storage, which holds data that is structured via tables, rows, and columns. The RDW’s compute power can be used only on its relational storage—they are tied together.</p>
</blockquote>
<hr />
<h3 id="heading-data-lake">Data Lake 🌊</h3>
<p><strong>What is it?</strong></p>
<p>A data lake stores raw, unprocessed data in its native format and uses a <mark>schema-on-read</mark> approach. It’s an excellent option for exploration and experimentation.</p>
<p><strong>Why use it?</strong></p>
<ul>
<li><p>Cost-effective storage for vast data volumes.</p>
</li>
<li><p>Flexible access for data scientists and power users.</p>
</li>
<li><p>Frees up enterprise data warehouse resources.</p>
</li>
<li><p>Retains complete historical data in one place.</p>
</li>
</ul>
<p><strong>🔍 Things to think about:</strong></p>
<ul>
<li><p>How do you plan to manage semi-structured or unstructured data?</p>
</li>
<li><p>Do you prioritize flexibility and experimentation for data users?</p>
</li>
</ul>
<hr />
<h3 id="heading-modern-data-warehouse-mdw">Modern Data Warehouse (MDW) ⚙️</h3>
<p><strong>How does it work?</strong></p>
<p>The modern data warehouse combines the strengths of RDWs and data lakes, offering:</p>
<ul>
<li><p>Low-latency, high-performance analytics.</p>
</li>
<li><p>Self-service business intelligence (BI) capabilities.</p>
</li>
<li><p>Interactive ad-hoc querying for business users.</p>
</li>
</ul>
<p><strong>Benefits:</strong></p>
<ul>
<li><p>Real-time data processing.</p>
</li>
<li><p>Compatibility with diverse data sources.</p>
</li>
<li><p>Enhanced compliance and security measures.</p>
</li>
</ul>
<p><strong>🔍 Ask yourself</strong></p>
<ul>
<li><p>Are your current analytics tools meeting business demands?</p>
</li>
<li><p>How effectively does your infrastructure support real-time data?</p>
</li>
</ul>
<hr />
<h3 id="heading-data-fabric">Data Fabric 🌐</h3>
<p><strong>What is it?</strong></p>
<p>Data fabric weaves together disparate data systems to create a unified, accessible layer. Think of it as a modern evolution of the traditional data warehouse, with added features like metadata cataloguing and data virtualization.</p>
<p><strong>Key Features:</strong></p>
<ul>
<li><p>Streamlined data access policies.</p>
</li>
<li><p>Support for real-time data handling.</p>
</li>
<li><p>Integration through APIs and microservices.</p>
</li>
</ul>
<p><strong>🔍 Questions to ponder:</strong></p>
<ul>
<li><p>How well do your systems integrate diverse data sources?</p>
</li>
<li><p>Is real-time data access critical for your organization?</p>
</li>
</ul>
<hr />
<h3 id="heading-data-lakehouse">Data Lakehouse 🏠</h3>
<p><strong>What is it?</strong></p>
<p>The data lakehouse bridges the gap between data lakes and RDWs, combining the scalability of a data lake with the transactional capabilities of a data warehouse.</p>
<p><strong>Key Features:</strong></p>
<ul>
<li><p>ACID transactions for data integrity.</p>
</li>
<li><p>Unified batch and streaming data processing.</p>
</li>
<li><p>Schema enforcement and evolution.</p>
</li>
</ul>
<p><strong>Who should use it?</strong></p>
<p>Organizations dealing with:</p>
<ul>
<li><p>Reliability issues between data lakes and warehouses.</p>
</li>
<li><p>Governance challenges for large-scale data.</p>
</li>
</ul>
<p><strong>🔍 Considerations:</strong></p>
<ul>
<li><p>How important are transactional guarantees for your workflows?</p>
</li>
<li><p>Are you facing persistent challenges with siloed data?</p>
</li>
</ul>
<hr />
<h3 id="heading-data-mesh">Data Mesh 🥅</h3>
<p><strong>What is it?</strong></p>
<p>Data mesh decentralizes data ownership, giving individual teams responsibility for their data while treating it as a product. This approach fosters scalability, agility, and collaboration.</p>
<p><strong>Key Principles:</strong></p>
<ol>
<li><p><strong>Domain Ownership:</strong> Teams closest to the data take responsibility.</p>
</li>
<li><p><strong>Data as a Product:</strong> Prioritize accessibility, quality, and usability.</p>
</li>
<li><p><strong>Self-Serve Infrastructure:</strong> Equip teams with tools to build and manage their pipelines.</p>
</li>
<li><p><strong>Federated Governance:</strong> Maintain consistency with centralized standards.</p>
</li>
</ol>
<p><strong>Why is it hard to implement?</strong></p>
<ol>
<li><p><strong>Cultural and Organizational Barriers:</strong></p>
<ul>
<li><p>Shifting responsibilities requires a mindset overhaul.</p>
</li>
<li><p>Teams must adopt a product-oriented view of data.</p>
</li>
<li><p>Resistance is common from teams used to centralized ownership.</p>
</li>
</ul>
</li>
<li><p><strong>Governance Complexity:</strong></p>
<ul>
<li><p>Federated governance is difficult to enforce across domains.</p>
</li>
<li><p>Maintaining interoperability and data quality is a significant challenge.</p>
</li>
<li><p>Without coordination, data silos or duplication may emerge.</p>
</li>
</ul>
</li>
<li><p><strong>Technical Hurdles:</strong></p>
<ul>
<li><p>Self-serve infrastructure tools are still evolving.</p>
</li>
<li><p>Performance issues arise when aggregating data from domains.</p>
</li>
<li><p>Requires highly skilled engineers within each domain.</p>
</li>
</ul>
</li>
</ol>
<p><strong>Why isn’t it more popular?</strong></p>
<ul>
<li><p><strong>High Cost:</strong> Organizational change and technical overhauls demand significant investment.</p>
</li>
<li><p><strong>Uncertain ROI:</strong> Benefits may take years to materialize, making it harder for companies to justify.</p>
</li>
<li><p><strong>Standardization Gaps:</strong> Lack of established tools and practices can result in inconsistent implementations.</p>
</li>
</ul>
<p><strong>Debates and Common Questions:</strong></p>
<ul>
<li><p><strong>Concept or Tool Agnostic?</strong> Data mesh is a conceptual framework that relies on principles rather than specific tools, sparking debates about standardization.</p>
</li>
<li><p><strong>Performance Concerns:</strong> Real-time insights can be delayed when aggregating data from multiple domains.</p>
</li>
</ul>
<p><strong>📈 Pros:</strong></p>
<ul>
<li><p>Encourages collaboration and accountability.</p>
</li>
<li><p>Scales effectively by leveraging domain expertise.</p>
</li>
<li><p>Improves overall data quality and usability.</p>
</li>
<li><p>Enhances agility by decentralizing ownership.</p>
</li>
</ul>
<p><strong>🔇 Cons:</strong></p>
<ul>
<li><p>High implementation and organizational costs.</p>
</li>
<li><p>Potential for data silos and duplication.</p>
</li>
<li><p>Requires skilled engineers and cultural buy-in.</p>
</li>
<li><p>Performance and interoperability challenges.</p>
</li>
</ul>
<p><strong>🔍 Is it right for you?</strong></p>
<ul>
<li><p>Does your organization have the resources and commitment for such a transformation?</p>
</li>
<li><p>Are your domain teams equipped with the necessary skills and tools?</p>
</li>
<li><p>How will you ensure governance across all domains?</p>
</li>
</ul>
<hr />
<h3 id="heading-when-to-use-each-architecture">When to Use Each Architecture ✅</h3>
<blockquote>
<p><mark>Choosing the right data architecture is highly context-dependent and influenced by various factors such as organizational size, data complexity, team expertise, and business goals. The following examples are not exhaustive but serve as a general guide to help you consider potential directions:</mark></p>
</blockquote>
<p><strong>Modern Data Warehouse:</strong> Ideal for organizations with smaller datasets and traditional business intelligence (BI) needs. It’s best suited for scenarios requiring low latency and familiar relational database tools.</p>
<p><strong>Data Fabric:</strong> Perfect for businesses needing to integrate diverse data sources. With its focus on real-time accessibility and governance, it’s a strong choice for enterprises managing complex systems.</p>
<p><strong>Data Lakehouse:</strong> A great fit for organizations prioritizing scalability, cost-effective storage, and advanced analytics. It offers a hybrid solution that balances flexibility and governance.</p>
<p><strong>Data Mesh:</strong> Best for large, domain-oriented companies struggling with scalability and data ownership issues. It’s ideal for organizations ready to invest in a cultural and technical transformation.</p>
<hr />
<ul>
<li><p>Does your organization have the resources and commitment for such a transformation?</p>
</li>
<li><p>Are your domain teams equipped with the necessary skills and tools?</p>
</li>
<li><p>How will you ensure governance across all domains?</p>
</li>
</ul>
<hr />
<h3 id="heading-comparison-of-data-architectures">Comparison of Data Architectures</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Characteristic</strong></td><td><strong>Relational Data Warehouse</strong></td><td><strong>Data Lake</strong></td><td><strong>Modern Data Warehouse</strong></td><td><strong>Data Fabric</strong></td><td><strong>Data Lakehouse</strong></td><td><strong>Data Mesh</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Year introduced</strong></td><td>1984</td><td>2010</td><td>2011</td><td>2016</td><td>2020</td><td>2019</td></tr>
<tr>
<td><strong>Centralized/Decentralized</strong></td><td>Centralized</td><td>Centralized</td><td>Centralized</td><td>Centralized</td><td>Centralized</td><td>Decentralized</td></tr>
<tr>
<td><strong>Storage type</strong></td><td>Relational</td><td>Object</td><td>Relational and object</td><td>Relational and object</td><td>Object</td><td>Domain-specific</td></tr>
<tr>
<td><strong>Schema type</strong></td><td>Schema-on-write</td><td>Schema-on-read</td><td>Schema-on-read and schema-on-write</td><td>Schema-on-read and schema-on-write</td><td>Schema-on-read</td><td>Domain-specific</td></tr>
<tr>
<td><strong>Data security</strong></td><td>High</td><td>Low to medium</td><td>Medium to high</td><td>High</td><td>Medium</td><td>Domain-specific</td></tr>
<tr>
<td><strong>Data latency</strong></td><td>Low</td><td>High</td><td>Low to high</td><td>Low to high</td><td>Medium to high</td><td>Domain-specific</td></tr>
<tr>
<td><strong>Time to Value</strong></td><td>Medium</td><td>Low</td><td>Low</td><td>Low</td><td>Low</td><td>High</td></tr>
<tr>
<td><strong>Total cost of the solution</strong></td><td>High</td><td>Low</td><td>Medium</td><td>Medium to high</td><td>Low to medium</td><td>High</td></tr>
<tr>
<td><strong>Supported use cases</strong></td><td>Low</td><td>Low to medium</td><td>Medium</td><td>Medium to high</td><td>High</td><td>High</td></tr>
<tr>
<td><strong>Difficulty of development</strong></td><td>Low</td><td>Medium</td><td>Medium</td><td>Medium</td><td>Medium to high</td><td>High</td></tr>
<tr>
<td><strong>Maturity of technology</strong></td><td>High</td><td>Medium</td><td>Medium to high</td><td>Medium to high</td><td>Medium to high</td><td>Low</td></tr>
<tr>
<td><strong>Company skill set needed</strong></td><td>Low</td><td>Low to medium</td><td>Medium</td><td>Medium to high</td><td>Medium to high</td><td>High</td></tr>
</tbody>
</table>
</div><blockquote>
<p>Most companies will use pieces of each architecture to build a solution adapted to their specific needs.</p>
</blockquote>
<h3 id="heading-final-thoughts">Final Thoughts 🌟</h3>
<p>Most organizations will find success in adopting a hybrid approach, blending aspects of these architectures to suit their unique needs. Each framework offers distinct benefits and challenges — the key is to evaluate your goals, resources, and scalability needs carefully. Which path will your organization choose?</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This blog only scratches the surface of what James Serra covers in his exceptional book, <em>Deciphering Data Architectures</em>. He provides an in-depth exploration of these topics, breaking them down across the following chapters:</div>
</div>

<ol>
<li><p><strong>Big Data</strong></p>
</li>
<li><p><strong>Types of Data Architectures</strong></p>
</li>
<li><p><strong>The Architecture Design Session</strong></p>
</li>
<li><p><strong>The Relational Data Warehouse</strong></p>
</li>
<li><p><strong>Data Lake</strong></p>
</li>
<li><p><strong>Data Storage Solutions and Processes</strong></p>
</li>
<li><p><strong>Approaches to Design</strong></p>
</li>
<li><p><strong>Approaches to Data Modeling</strong></p>
</li>
<li><p><strong>Approaches to Data Ingestion</strong></p>
</li>
<li><p><strong>The Modern Data Warehouse</strong></p>
</li>
<li><p><strong>Data Fabric</strong></p>
</li>
<li><p><strong>Data Lakehouse</strong></p>
</li>
<li><p><strong>Data Mesh Foundation</strong></p>
</li>
<li><p><strong>Should You Adopt Data Mesh? Myths, Concerns, and the Future</strong></p>
</li>
<li><p><strong>People and Processes</strong></p>
</li>
<li><p><strong>Technologies</strong></p>
</li>
</ol>
<p>Each chapter delves into key concepts, offering valuable insights and practical guidance. For anyone navigating the world of data architecture, this book is a must-read.</p>
<p><strong>Thanks for reading!</strong> 😊</p>
]]></content:encoded></item><item><title><![CDATA[🚀 Supercharge Your Data Migration with the Right Azure VM: A Guide for Massive Data Transfers!]]></title><description><![CDATA[Migrating terabytes of data from on-premises servers to the cloud is no small feat. The success of such an operation heavily depends on choosing the right infrastructure. This is where Azure Virtual Machines (VMs) come in, providing the flexibility, ...]]></description><link>https://bidiaries.com/supercharge-your-data-migration-with-the-right-azure-vm-a-guide-for-massive-data-transfers</link><guid isPermaLink="true">https://bidiaries.com/supercharge-your-data-migration-with-the-right-azure-vm-a-guide-for-massive-data-transfers</guid><category><![CDATA[Azure]]></category><category><![CDATA[virtual machine]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 17 Dec 2024 16:23:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734452379296/64c77b1a-919b-402c-8c7c-41d86cde9bf1.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Migrating <strong>terabytes of data</strong> from on-premises servers to the cloud is no small feat. The success of such an operation heavily depends on choosing the right infrastructure. This is where <strong>Azure Virtual Machines (VMs)</strong> come in, providing the flexibility, scalability, and performance needed for <strong>data migrations</strong> and <strong>cloud publishing</strong> tasks.</p>
<p>But how do you pick the best Azure VM for your migration needs? 🤔 With numerous VM types available, understanding their strengths and use cases is critical. Let’s dive into the details of <strong>Azure VM series</strong> and explore how to optimize your data migration process—whether you're pulling data from on-premise sources or moving it into <strong>Microsoft Fabric</strong> or other cloud solutions. 🌐</p>
<hr />
<h3 id="heading-azure-vm-categories-for-data-migration">🔄 <strong>Azure VM Categories for Data Migration</strong></h3>
<p>Azure VMs are categorized based on their ability to handle specific workloads. For <strong>data migration</strong>, your choice should consider factors like <strong>compute power</strong>, <strong>memory capacity</strong>, and <strong>disk performance</strong>. Below, we analyze the most relevant VM series and their application in data migration tasks.</p>
<hr />
<h3 id="heading-1-dv5-and-dsv5-series-general-purpose">1. <strong>Dv5 and Dsv5 Series: General Purpose</strong></h3>
<p>The <strong>Dv5/Dsv5 series</strong> provides a <strong>balanced CPU-to-memory ratio</strong>, making it suitable for a wide variety of workloads. These VMs are ideal for <strong>small to medium data migrations</strong> where cost efficiency and performance balance are key.</p>
<h4 id="heading-use-case"><strong>Use Case</strong>:</h4>
<ul>
<li><p><strong>Small to Medium Data Migrations</strong>: Handle datasets under 1TB with moderate compute and memory needs.</p>
</li>
<li><p><strong>Cost Efficiency</strong>: Ideal for running basic SQL queries or data integrations.</p>
</li>
</ul>
<h4 id="heading-vm-sizes"><strong>VM Sizes</strong>:</h4>
<ul>
<li><p><strong>D16s_v5</strong>: 16 vCPUs, 64 GB RAM – suited for medium workloads.</p>
</li>
<li><p><strong>D32s_v5</strong>: 32 vCPUs, 128 GB RAM – handles larger datasets efficiently.</p>
</li>
</ul>
<hr />
<h3 id="heading-2-ev5-and-esv5-series-memory-optimized">2. <strong>Ev5 and Esv5 Series: Memory Optimized</strong></h3>
<p>The <strong>Ev5/Esv5 series</strong> is designed for <strong>memory-intensive tasks</strong>, making it a great choice for handling <strong>large datasets</strong> and <strong>complex transformations</strong>. These VMs offer a higher memory-to-vCPU ratio.</p>
<h4 id="heading-use-case-1"><strong>Use Case</strong>:</h4>
<ul>
<li><p><strong>Large Data Transformations</strong>: Suitable for operations like joining datasets, cleaning data, or handling big XML/JSON files.</p>
</li>
<li><p><strong>In-Memory Operations</strong>: Use tools that require significant RAM for processing.</p>
</li>
</ul>
<h4 id="heading-vm-sizes-1"><strong>VM Sizes</strong>:</h4>
<ul>
<li><p><strong>E16s_v5</strong>: 16 vCPUs, 128 GB RAM – perfect for memory-heavy workloads.</p>
</li>
<li><p><strong>E64s_v5</strong>: 64 vCPUs, 512 GB RAM – excellent for larger datasets and in-memory computations.</p>
</li>
</ul>
<hr />
<h3 id="heading-3-fsv2-series-compute-optimized">3. <strong>Fsv2 Series: Compute Optimized</strong></h3>
<p>The <strong>Fsv2 series</strong> focuses on <strong>compute-heavy workloads</strong>, with a high vCPU-to-memory ratio. This makes it ideal for <strong>data compression</strong>, <strong>encryption</strong>, and other CPU-intensive tasks.</p>
<h4 id="heading-use-case-2"><strong>Use Case</strong>:</h4>
<ul>
<li><p><strong>Compute-Intensive Tasks</strong>: Accelerate operations like bulk copy (BCP) and parallel data extraction.</p>
</li>
<li><p><strong>Batch Data Processing</strong>: Process large datasets quickly with minimal memory overhead.</p>
</li>
</ul>
<h4 id="heading-vm-sizes-2"><strong>VM Sizes</strong>:</h4>
<ul>
<li><p><strong>F32s_v2</strong>: 32 vCPUs, 64 GB RAM – excellent for compute-heavy extraction and transformations.</p>
</li>
<li><p><strong>F64s_v2</strong>: 64 vCPUs, 128 GB RAM – ideal for demanding parallel processing.</p>
</li>
</ul>
<hr />
<h3 id="heading-4-lsv2-series-storage-optimized">4. <strong>Lsv2 Series: Storage Optimized</strong></h3>
<p>The <strong>Lsv2 series</strong> is tailored for <strong>storage-heavy tasks</strong>, offering <strong>NVMe drives</strong> with high throughput and low latency. These VMs are perfect for <strong>data staging</strong>, <strong>temporary storage</strong>, and IO-intensive operations.</p>
<h4 id="heading-use-case-3"><strong>Use Case</strong>:</h4>
<ul>
<li><p><strong>Large Data Transfers</strong>: Staging multi-terabyte datasets before cloud transfer.</p>
</li>
<li><p><strong>High IO Workloads</strong>: Optimized for fast, temporary file handling during migration.</p>
</li>
</ul>
<h4 id="heading-vm-sizes-3"><strong>VM Sizes</strong>:</h4>
<ul>
<li><p><strong>L32s_v2</strong>: 32 vCPUs, 256 GB RAM, 4x2TB NVMe – for large-scale migration prep.</p>
</li>
<li><p><strong>L80s_v2</strong>: 80 vCPUs, 640 GB RAM, 10x2TB NVMe – suitable for extreme IO tasks.</p>
</li>
</ul>
<hr />
<h3 id="heading-5-hbv4-and-hc-series-hpc-optimized">5. <strong>HBv4 and HC Series: HPC Optimized</strong></h3>
<p>The <strong>HBv4 and HC series</strong> are built for <strong>high-performance computing (HPC)</strong>. They are ideal for <strong>mission-critical migrations</strong> requiring massive parallelism and real-time data transformations.</p>
<h4 id="heading-use-case-4"><strong>Use Case</strong>:</h4>
<ul>
<li><p><strong>Ultra-Large Datasets</strong>: Migrate multi-petabyte datasets with high-speed throughput.</p>
</li>
<li><p><strong>Real-Time Processing</strong>: Execute analytics and transformations during migration.</p>
</li>
</ul>
<h4 id="heading-vm-sizes-4"><strong>VM Sizes</strong>:</h4>
<ul>
<li><p><strong>HB120rs_v4</strong>: 120 vCPUs, 480 GB RAM – for parallelized data migration.</p>
</li>
<li><p><strong>HC44rs</strong>: 44 vCPUs, 352 GB RAM – handles extreme compute tasks.</p>
</li>
</ul>
<hr />
<h3 id="heading-azure-vm-series-comparison-cost-vs-performance-for-data-migration">🔍 <strong>Azure VM Series Comparison: Cost vs. Performance for Data Migration</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>VM Series</strong></td><td><strong>Use Case</strong></td><td><strong>vCPU:Memory Ratio</strong></td><td><strong>Cost</strong></td><td><strong>Recommended For</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Dv5/Dsv5</strong></td><td>Balanced workloads, moderate migrations</td><td>1:4</td><td>Low to Moderate</td><td>Small to medium datasets, cost-conscious operations</td></tr>
<tr>
<td><strong>Ev5/Esv5</strong></td><td>Memory-heavy data processing</td><td>1:8</td><td>Moderate</td><td>Large datasets with in-memory transformations</td></tr>
<tr>
<td><strong>Fsv2</strong></td><td>Compute-heavy tasks, batch processing</td><td>2:1</td><td>Moderate</td><td>High CPU needs like bulk copy (BCP) and parallel processing</td></tr>
<tr>
<td><strong>Lsv2</strong></td><td>IO-intensive tasks, fast local storage</td><td>1:8 with NVMe</td><td>High</td><td>Multi-terabyte data transfers with high disk IO requirements</td></tr>
<tr>
<td><strong>HBv4/HC</strong></td><td>Ultra-large datasets, HPC workloads</td><td>1:4 to 1:8</td><td>Very High</td><td>Mission-critical, real-time processing, massive parallelism</td></tr>
</tbody>
</table>
</div><hr />
<h3 id="heading-final-considerations">🚀 <strong>Final Considerations</strong></h3>
<p>The <strong>right Azure VM series</strong> depends on your <strong>dataset size</strong>, <strong>complexity of operations</strong>, and <strong>cost constraints</strong>. To summarize:</p>
<ul>
<li><p><strong>Cost-Conscious?</strong> Start with <strong>Dv5/Dsv5</strong> for balanced performance.</p>
</li>
<li><p><strong>Memory-Heavy?</strong> Choose <strong>Ev5/Esv5</strong> for complex transformations.</p>
</li>
<li><p><strong>Compute-Intensive?</strong> Opt for <strong>Fsv2</strong>.</p>
</li>
<li><p><strong>Storage-Heavy?</strong> Go with <strong>Lsv2</strong> for IO-heavy workloads.</p>
</li>
<li><p><strong>Mission-Critical?</strong> Use <strong>HBv4/HC</strong> for extreme-scale, high-performance needs.</p>
</li>
</ul>
<hr />
<h3 id="heading-why-choose-cloud-over-on-prem">🌐 <strong>Why Choose Cloud over On-Prem?</strong></h3>
<p>While on-premises servers might seem like an alternative, they struggle to match Azure’s scalability, cost-efficiency, and flexibility. Azure VMs offer <strong>pay-as-you-go pricing</strong>, quick provisioning, and <strong>built-in scalability</strong>, making them a superior choice for most data migration scenarios.</p>
<hr />
<p>By carefully choosing the right Azure VM, you can migrate your data faster, more efficiently, and at the right cost. Whether it’s <strong>2TB</strong> or <strong>20TB</strong>, Azure’s flexibility ensures your migration is a success.</p>
<blockquote>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview?tabs=breakdownseries%2Cgeneralsizelist%2Ccomputesizelist%2Cmemorysizelist%2Cstoragesizelist%2Cgpusizelist%2Cfpgasizelist%2Chpcsizelist#general-purpose">Further Read</a></p>
</blockquote>
<p><strong>Thanks For Reading !!!</strong> 👍</p>
]]></content:encoded></item><item><title><![CDATA[How Databricks Plays Nicely with All Major Clouds: Azure, AWS, and GCP ✨]]></title><description><![CDATA[If you've been working in the data world, you've probably heard the name Databricks thrown around—and for good reason! Built on top of Apache Spark, Databricks is a powerhouse for big data processing, machine learning, and analytics. But here's the m...]]></description><link>https://bidiaries.com/how-databricks-plays-nicely-with-all-major-clouds-azure-aws-and-gcp</link><guid isPermaLink="true">https://bidiaries.com/how-databricks-plays-nicely-with-all-major-clouds-azure-aws-and-gcp</guid><category><![CDATA[Databricks]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[AWS]]></category><category><![CDATA[GCP]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 17 Dec 2024 15:28:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UF4fp9YRZF4/upload/d90a5fa9a3f4ae766db475b6b201c2ac.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've been working in the data world, you've probably heard the name <strong>Databricks</strong> thrown around—and for good reason! Built on top of Apache Spark, Databricks is a <strong>powerhouse</strong> for big data processing, machine learning, and analytics. But here's the magic:</p>
<p>Databricks isn't tied to one cloud—it works seamlessly on <strong>Azure</strong>, <strong>AWS</strong>, and <strong>Google Cloud (GCP)</strong>. Let's dive into how Databricks plugs and plays across these clouds and what each cloud vendor offers to make it feel like <em>their own</em>. ✨</p>
<hr />
<h2 id="heading-what-does-cloud-agnostic-mean">📦 <strong>What Does "Cloud-Agnostic" Mean?</strong></h2>
<p>The term <strong>cloud-agnostic</strong> means Databricks is not locked into any single cloud provider. Instead, it can run smoothly across multiple clouds while keeping the same core technology, UI, and user experience.</p>
<p>Imagine this: Databricks is like a <strong>universal app</strong> that can run on any smartphone—whether it's iOS (Azure), Android (AWS), or Google Pixel (GCP). The app works the same, but each phone adds a little <em>flavor</em> to the experience.</p>
<p>For Databricks, these cloud-specific flavours come from integrations like storage, compute, and security services. Now, let's see how Azure, AWS, and GCP make Databricks their own.</p>
<hr />
<h2 id="heading-databricks-on-azure-azure-databricks">🛠️ <strong>Databricks on Azure (Azure Databricks)</strong></h2>
<p>If you're deep into the Microsoft ecosystem, <strong>Azure Databricks</strong> is the perfect fit. Azure Databricks is a fully managed service that combines Databricks' innovation with Azure's capabilities.</p>
<h3 id="heading-why-azure-databricks-stands-out">Why Azure Databricks Stands Out:</h3>
<ul>
<li><p><strong>Deep Integration with Azure Services</strong>: Works natively with Azure Data Lake Storage (ADLS), Azure Synapse, Power BI, and Azure Machine Learning.</p>
</li>
<li><p><strong>Azure Active Directory (AAD)</strong>: Enterprise-grade security with single sign-on (SSO) and role-based access control (RBAC).</p>
</li>
<li><p><strong>Optimized for Azure Compute</strong>: Databricks clusters run on Azure VMs (virtual machines), giving you a streamlined setup.</p>
</li>
<li><p><strong>Unified Analytics</strong>: Perfect for companies using Power BI as the reporting layer on top of their Databricks-powered data lake.</p>
</li>
</ul>
<p><strong>Cost Segregation</strong>: Azure Databricks costs include compute (VMs), storage (ADLS), and Databricks service charges, all billed under the Azure portal.</p>
<p>Think of Azure Databricks as a <em>tailor-made suit</em> for Microsoft shops—it just fits perfectly.</p>
<hr />
<h2 id="heading-databricks-on-aws-aws-databricks">🚀 <strong>Databricks on AWS (AWS Databricks)</strong></h2>
<p>For companies already invested in the Amazon Web Services (AWS) ecosystem, <strong>AWS Databricks</strong> is the go-to choice. AWS was the <strong>first cloud provider</strong> to partner with Databricks!</p>
<h3 id="heading-why-aws-databricks-stands-out">Why AWS Databricks Stands Out:</h3>
<ul>
<li><p><strong>Storage Integration</strong>: Works seamlessly with Amazon S3 (Simple Storage Service), which is AWS's backbone for cloud storage.</p>
</li>
<li><p><strong>Compute Power</strong>: Databricks clusters leverage EC2 instances, which are easy to scale up and down.</p>
</li>
<li><p><strong>Security and IAM</strong>: Databricks integrates with AWS Identity and Access Management (IAM) for fine-grained security.</p>
</li>
<li><p><strong>Native AWS Tools</strong>: Connect easily with Redshift, Glue, and SageMaker for a complete data and AI pipeline.</p>
</li>
</ul>
<p><strong>Cost Segregation</strong>: AWS Databricks costs are split between S3 for storage, EC2 for compute, and Databricks service charges managed within the AWS billing dashboard.</p>
<p>AWS Databricks feels like a <em>high-performance sports car</em> running on Amazon's robust infrastructure—fast, flexible, and reliable.</p>
<hr />
<h2 id="heading-databricks-on-google-cloud-gcp-databricks">📑 <strong>Databricks on Google Cloud (GCP Databricks)</strong></h2>
<p>Google Cloud has entered the Databricks game more recently, but it brings some serious strengths to the table. <strong>GCP Databricks</strong> integrates nicely with Google's analytics and AI/ML offerings.</p>
<h3 id="heading-why-gcp-databricks-stands-out">Why GCP Databricks Stands Out:</h3>
<ul>
<li><p><strong>Google Cloud Storage (GCS)</strong>: Acts as the primary storage layer for Databricks clusters.</p>
</li>
<li><p><strong>BigQuery Integration</strong>: Connect Databricks with BigQuery for data warehousing and analytics.</p>
</li>
<li><p><strong>Vertex AI</strong>: Combine Databricks with Vertex AI for end-to-end machine learning workflows.</p>
</li>
<li><p><strong>Scalable Compute</strong>: Databricks clusters run on GCP Compute Engine, which offers flexibility and performance.</p>
</li>
<li><p><strong>Data-Driven Organizations</strong>: Ideal for companies already invested in Google's AI and big data tools.</p>
</li>
</ul>
<p><strong>Cost Segregation</strong>: GCP Databricks involves costs for GCS (storage), Compute Engine (clusters), and Databricks service charges, all billed under the Google Cloud console.</p>
<p>GCP Databricks feels like a <em>cutting-edge tech lab</em>—it thrives in environments where AI/ML innovation is the focus.</p>
<hr />
<h2 id="heading-why-do-databricks-work-across-all-clouds">🌎 <strong>Why Do Databricks Work Across All Clouds?</strong></h2>
<p>So how does Databricks pull this off? Here’s the secret sauce:</p>
<ol>
<li><p><strong>Standardized Architecture</strong>: Databricks uses containerization (like Docker) and Kubernetes to make its platform portable across different clouds.</p>
</li>
<li><p><strong>Unified User Experience</strong>: Whether on Azure, AWS, or GCP, the Databricks interface, APIs, and notebooks remain the same.</p>
</li>
<li><p><strong>Decoupled Storage and Compute</strong>: Databricks can connect to <em>any cloud storage</em> (S3, ADLS, GCS) while managing compute clusters natively.</p>
</li>
<li><p><strong>Partnerships</strong>: Databricks partners closely with Microsoft, AWS, and Google Cloud to provide deep integrations and managed services.</p>
</li>
</ol>
<hr />
<h2 id="heading-which-cloud-should-you-choose-for-databricks">📊 <strong>Which Cloud Should You Choose for Databricks?</strong></h2>
<p>The best cloud for Databricks depends on <strong>your existing investments</strong> and business needs:</p>
<ul>
<li><p><strong>Azure Databricks</strong>: Ideal for organizations deep into Microsoft Azure and Power BI.</p>
</li>
<li><p><strong>AWS Databricks</strong>: Perfect for AWS-heavy environments with S3 and EC2 at the core.</p>
</li>
<li><p><strong>GCP Databricks</strong>: Best for organizations focused on AI/ML innovation using Google tools like BigQuery and Vertex AI.</p>
</li>
</ul>
<p>At the end of the day, Databricks gives you <strong>freedom of choice</strong> while delivering the same powerful platform wherever you go. ✅</p>
<hr />
<h2 id="heading-the-bottom-line">📢 <strong>The Bottom Line</strong></h2>
<p>Databricks is like the <strong>ultimate team player</strong>—it works on Azure, AWS, and GCP without skipping a beat. Each cloud vendor adds its own flavor to Databricks with integrations like storage, compute, and security tools.</p>
<p>The result? You get a <strong>consistent, cloud-agnostic experience</strong> for big data, machine learning, and analytics—no matter where your data lives. ✨</p>
<p><strong>Cost Segregation</strong> Summary:</p>
<ul>
<li><p><strong>Azure</strong>: Compute (VMs), ADLS (storage), and Databricks fees.</p>
</li>
<li><p><strong>AWS</strong>: EC2 (compute), S3 (storage), and Databricks fees.</p>
</li>
<li><p><strong>GCP</strong>: Compute Engine (compute), GCS (storage), and Databricks fees.</p>
</li>
</ul>
<p>So, whether you’re an <strong>Azure loyalist</strong>, an <strong>AWS powerhouse</strong>, or a <strong>GCP innovator</strong>, Databricks has you covered.</p>
<p><strong>Thanks For Reading !!! 👍</strong></p>
]]></content:encoded></item><item><title><![CDATA[🌟 Integrating Managed Identity(MI) and Service Principal for Secure Data Movement: From Azure SQL to Microsoft Fabric via Key Vault 🔒]]></title><description><![CDATA[In the world of Azure, managing secure connections between services is critical for building scalable and secure cloud solutions. 🚀 One key tool to achieve this is Managed Identity (MI). But what is it, and how does it work in the context of Azure D...]]></description><link>https://bidiaries.com/integrating-managed-identitymi-and-service-principal-for-secure-data-movement-from-azure-sql-to-microsoft-fabric-via-key-vault</link><guid isPermaLink="true">https://bidiaries.com/integrating-managed-identitymi-and-service-principal-for-secure-data-movement-from-azure-sql-to-microsoft-fabric-via-key-vault</guid><category><![CDATA[Azure]]></category><category><![CDATA[azure certified]]></category><category><![CDATA[medium]]></category><category><![CDATA[service principal]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Security]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[data]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Sat, 14 Dec 2024 09:46:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/yekGLpc3vro/upload/0540a0815e0410ce30354a15ac33521c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the world of Azure, managing secure connections between services is <strong>critical</strong> for building scalable and secure cloud solutions. 🚀 One key tool to achieve this is <strong>Managed Identity (MI)</strong>. But what is it, and how does it work in the context of Azure Data Factory (ADF), Key Vault, and other Azure services? 🤔 This blog will break it down, using simple analogies and practical examples to demystify the concept.</p>
<hr />
<h2 id="heading-what-is-a-managed-identity">🤖 <strong>What is a Managed Identity?</strong></h2>
<p>A <strong>Managed Identity (MI)</strong> is a feature provided by Azure that allows an Azure resource (like ADF, Virtual Machines, or App Services) to authenticate securely with other Azure services without requiring credentials such as usernames, passwords, or client secrets.</p>
<p>Think of a Managed Identity as a <strong>special type of account in Azure Active Directory (Azure AD)</strong>. It is:</p>
<ul>
<li><p>🔄 Automatically managed by Azure</p>
</li>
<li><p>🔗 Tied directly to the Azure resource</p>
</li>
<li><p>🎫 Used to acquire tokens for secure authentication with Azure services</p>
</li>
</ul>
<hr />
<h2 id="heading-how-does-it-work-in-azure-data-factory-adf">🚚 <strong>How Does It Work in Azure Data Factory (ADF)?</strong></h2>
<p>When you enable Managed Identity for an ADF instance, Azure assigns it a unique identity in Azure AD. This identity allows ADF to securely access other Azure services, such as <strong>Azure Key Vault</strong>, <strong>Azure SQL Database</strong>, or <strong>Azure Blob Storage</strong>, without requiring hardcoded credentials.</p>
<p>Here's a step-by-step flow of how Managed Identity works in ADF:</p>
<ol>
<li><p><strong>Enable Managed Identity</strong> 🟢</p>
<ul>
<li>ADF is given a unique Managed Identity by Azure</li>
</ul>
</li>
<li><p><strong>Assign Permissions</strong> 🔑</p>
<ul>
<li><p>Grant this identity the necessary permissions to access resources like Key Vault or Azure SQL</p>
</li>
<li><p>This is done either via Access Policies or Azure RBAC</p>
</li>
</ul>
</li>
<li><p><strong>Token-Based Authentication</strong> 🎫</p>
<ul>
<li><p>When ADF needs to access a resource, it requests an authentication token from Azure AD</p>
</li>
<li><p>The resource (e.g., Key Vault) validates the token to ensure the identity has the necessary permissions</p>
</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-an-analogy-to-simplify">📦 <strong>An Analogy to Simplify</strong></h2>
<p><strong>Problem: Securely Transferring Sensitive Customer Data</strong> 🚨</p>
<p>Imagine the below analogy.<br />You're running a data logistics company facing a critical challenge: securely transferring sensitive customer records from your primary database to a new, advanced data warehouse, without exposing critical connection details or risking credential leaks. 🔒</p>
<p>Your specific mission:</p>
<ul>
<li><p>📊 Move customer data from Azure SQL Database to Microsoft Fabric</p>
</li>
<li><p>🛡️ Ensure zero credential exposure</p>
</li>
<li><p>🔐 Maintain end-to-end security throughout the transfer</p>
</li>
<li><p>🤖 Automate the process without manual intervention</p>
</li>
</ul>
<p>The solution involves a specialized secure transport system:</p>
<ul>
<li><p><strong>ADF (Armored Transport Vehicle)</strong>: A secure data movement mechanism with a special security badge (Managed Identity) 🚚</p>
</li>
<li><p><strong>Key Vault (Secure Key Vault)</strong>: Stores encrypted connection credentials and access keys 🔑</p>
</li>
<li><p><strong>Fabric (Secure Vault)</strong>: High-security destination that accepts both special badges (Managed Identity) and traditional security passes (Service Principal) depending on your setup 🏦</p>
</li>
</ul>
<p>Data Transfer Security Flow:</p>
<ol>
<li><p>🛡️ The secure transport (ADF) uses its official badge (Managed Identity) to access the secure key locker (Azure Key Vault)</p>
</li>
<li><p>🔓 The key locker verifies the badge and provides temporary, encrypted access credentials</p>
</li>
<li><p>📦 Using these secure credentials, the transport safely extracts customer records from the source database (Azure SQL Database)</p>
</li>
<li><p>🚚 The transport approaches the advanced data vault (Microsoft Fabric)</p>
</li>
<li><p>🔐 The vault verifies authentication using either:</p>
<ul>
<li><p>The transport's special badge (Managed Identity) for Azure-integrated scenarios</p>
</li>
<li><p>A traditional security pass (Service Principal) for specific configurations or hybrid scenarios</p>
</li>
</ul>
</li>
</ol>
<p>Key Security Objectives Achieved:</p>
<ul>
<li><p>🚫 No hardcoded credentials</p>
</li>
<li><p>🛡️ Automated, secure authentication</p>
</li>
<li><p>💡 Dynamic, temporary access tokens</p>
</li>
<li><p>🔒 Elimination of credential management overhead</p>
</li>
</ul>
<p>This approach transforms a potentially risky manual data transfer into a seamless, secure, and automated process. 🚀</p>
<hr />
<h2 id="heading-why-use-managed-identity">🛡️ <strong>Why Use Managed Identity?</strong></h2>
<p>Managed Identity eliminates the need to manage credentials manually. Instead, Azure takes care of securely handling authentication, which reduces the risk of credential leakage or expiration. Key benefits include:</p>
<ol>
<li><p><strong>No Hardcoded Credentials</strong> 🚫</p>
<ul>
<li>Passwords, client secrets, or keys are no longer needed</li>
</ul>
</li>
<li><p><strong>Lifecycle Management</strong> 🔄</p>
<ul>
<li><p>Managed Identity is tied to the lifecycle of the Azure resource</p>
</li>
<li><p>Deleting the resource also deletes the associated identity</p>
</li>
</ul>
</li>
<li><p><strong>Enhanced Security</strong> 🔒</p>
<ul>
<li>Tokens are automatically rotated and managed by Azure</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-how-adf-uses-managed-identity-with-azure-key-vault">🔑 <strong>How ADF Uses Managed Identity with Azure Key Vault</strong></h2>
<h3 id="heading-scenario">Scenario:</h3>
<p>You want ADF to securely fetch secrets (e.g., database connection strings) from Azure Key Vault without hardcoding any credentials.</p>
<h3 id="heading-steps">Steps:</h3>
<ol>
<li><p><strong>Enable System-Assigned Managed Identity for ADF</strong> 🟢</p>
<ul>
<li><p>In ADF's configuration, enable the System-Assigned Managed Identity</p>
</li>
<li><p>This assigns a unique identity to the ADF instance</p>
</li>
</ul>
</li>
<li><p><strong>Grant Access to Key Vault</strong> 🔓</p>
<ul>
<li><p>In Key Vault, add an Access Policy or use Azure RBAC to grant permissions by searching for your <strong>Azure Data Factory</strong> instance</p>
</li>
<li><p>💡 <strong><mark>Important Note</mark></strong><mark>: When you select "Azure Data Factory" in the access policies or RBAC assignments, you're actually selecting its Managed Identity! Azure shows you the ADF name for simplicity, but behind the scenes, it's granting permissions to the Managed Identity associated with that ADF instance</mark></p>
</li>
<li><p>Typically, grant <code>Get</code> and <code>List</code> permissions for secrets</p>
</li>
</ul>
</li>
<li><p><strong>Use Key Vault in Linked Services</strong> 🔗</p>
<ul>
<li><p>When creating a Linked Service in ADF, choose Key Vault as the authentication method</p>
</li>
<li><p>ADF uses its Managed Identity to fetch the secrets at runtime</p>
</li>
</ul>
</li>
<li><p><strong>Token-Based Authentication</strong> 🎫</p>
<ul>
<li><p>At runtime, ADF uses its Managed Identity to request an access token from Azure AD</p>
</li>
<li><p>Azure AD validates the identity and issues a token</p>
</li>
<li><p>Key Vault uses the token to verify permissions and grants access to the requested secrets</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-azure-data-factory-managed-identity-to-key-vault-authentication-flow">Azure Data Factory Managed Identity to Key Vault Authentication Flow:</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734169311278/fbded311-6495-499e-9a34-d63e5e1da21d.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-managed-identity-vs-service-principal">🆚 <strong>Managed Identity vs. Service Principal</strong></h2>
<blockquote>
<p>Service Principal ID sometimes referred as Application(client) ID.</p>
</blockquote>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Managed Identity</strong> 🤖</td><td><strong>Service Principal</strong> 🛡️</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Creation</strong></td><td>Automatically created by Azure for a resource</td><td>Manually created in Azure AD by the user</td></tr>
<tr>
<td><strong>Credential Management</strong></td><td>Fully managed by Azure (no secrets to manage)</td><td>Requires manual management of client secrets</td></tr>
<tr>
<td><strong>Lifecycle</strong></td><td>Tied to the Azure resource lifecycle</td><td>Independent of the resource lifecycle</td></tr>
<tr>
<td><strong>Use Case</strong></td><td>Best for scenarios within Azure (e.g., Key Vault)</td><td>Useful for hybrid environments (e.g., on-premises apps)</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-common-questions-about-managed-identity">❓ <strong>Common Questions About Managed Identity</strong></h2>
<h4 id="heading-1-is-managed-identity-a-user-or-account">1. <strong>Is Managed Identity a User or Account?</strong> 🤨</h4>
<p>No, Managed Identity is not a user. It is a type of Azure AD identity assigned to an Azure resource (like ADF) to enable secure authentication.</p>
<h4 id="heading-2-when-i-grant-key-vault-access-to-adf-what-happens">2. <strong>When I Grant Key Vault Access to ADF, What Happens?</strong> 🔓</h4>
<p>When you grant Key Vault access to ADF, you're actually granting access to the <strong>Managed Identity</strong> tied to that specific ADF instance. This allows ADF to authenticate securely without requiring credentials.</p>
<h4 id="heading-3-can-i-use-managed-identity-instead-of-service-principal">3. <strong>Can I Use Managed Identity Instead of Service Principal?</strong> 🔄</h4>
<p>Yes, in most Azure-native scenarios, Managed Identity is preferred because it eliminates credential management and improves security. However, Service Principals may still be needed for hybrid setups.</p>
<h4 id="heading-4-how-does-microsoft-fabric-handle-authentication">4. <strong>How Does Microsoft Fabric Handle Authentication?</strong> 🎯</h4>
<p>Microsoft Fabric supports both Managed Identity and Service Principal authentication. For Azure-native workflows, Managed Identity provides seamless integration, while Service Principal offers flexibility for specific scenarios like hybrid deployments or custom applications. Choose based on your specific integration needs and security requirements.</p>
<h4 id="heading-5-whats-the-difference-between-system-assigned-and-user-assigned-managed-identities">5. <strong>What's the Difference Between System-assigned and User-assigned Managed Identities?</strong> 🤔</h4>
<ul>
<li><p><strong>System-assigned</strong>: Automatically created and managed by Azure, tied directly to one resource's lifecycle (like your ADF instance)</p>
</li>
<li><p><strong>User-assigned</strong>: Created as standalone Azure resources, can be assigned to multiple resources, and managed independently</p>
</li>
</ul>
<hr />
<h2 id="heading-conclusion">🏁 <strong>Conclusion</strong></h2>
<p>Managed Identity simplifies secure authentication in Azure by eliminating the need for manual credential management. When used with Azure Data Factory, it ensures a seamless and secure flow of data between services like Azure SQL Database, Key Vault, and Microsoft Fabric.</p>
<p>By understanding and leveraging Managed Identity, you can build secure, scalable, and efficient cloud workflows while reducing operational overhead. 💡</p>
<p><strong>Have questions? Drop a comment below!</strong> 💬</p>
<p>Thanks for Reading !!!👍</p>
]]></content:encoded></item><item><title><![CDATA[Maximizing Microsoft Fabric Capacity and PowerBI Licensing Cost:]]></title><description><![CDATA[As businesses grow, so do their reporting needs, and managing Microsoft Fabric capacity alongside Power BI licensing can become quite complex. With the increasing demand for robust data models, seamless report generation, and broad access across user...]]></description><link>https://bidiaries.com/maximizing-microsoft-fabric-capacity-and-powerbi-licensing-cost</link><guid isPermaLink="true">https://bidiaries.com/maximizing-microsoft-fabric-capacity-and-powerbi-licensing-cost</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[PowerPlatform]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Tue, 10 Dec 2024 11:34:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/o-MyHqEEHoM/upload/62db401600272ef425a3c74f28fca549.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>As businesses grow, so do their reporting needs, and managing Microsoft Fabric capacity alongside Power BI licensing can become quite complex. With the increasing demand for robust data models, seamless report generation, and broad access across user bases, it’s essential to make the right choices around capacity and licensing to ensure performance, scalability, and cost efficiency.</p>
<p>In this blog post, we’ll dive into some common questions surrounding <strong>Microsoft Fabric capacity</strong> and <strong>Power BI Pro licenses</strong>. We’ll explore the best practices for managing capacities, the intricacies of semantic model access, and the most effective ways to handle a large user base without overspending on individual licenses.</p>
<p>Let’s break this down through a series of questions and answers to provide a clear roadmap for navigating your Power BI and Fabric capacity needs.</p>
<hr />
<h3 id="heading-q1-i-have-an-f2f4f8f16f32any-one-of-these-fabric-capacity-used-for-running-pipelines-and-notebooks-etc-and-creating-semantic-models-in-onelake-can-power-bi-pro-users-access-reports-and-connect-to-these-models-even-if-they-are-not-assigned-to-a-fabric-capacity"><strong>Q1: I have an F2/F4/F8/F16/F32(any one of these) Fabric capacity used for running pipelines, and notebooks etc and creating semantic models in OneLake. Can Power BI Pro users access reports and connect to these models even if they are not assigned to a Fabric capacity?</strong></h3>
<p><strong>A: Yes, with the right permissions in place.</strong></p>
<p>Your <mark>F2 or any which is less than F64</mark> SKU, Fabric capacity is doing the heavy lifting by handling pipelines, data storage, and creating semantic models in <strong>OneLake</strong>. Power BI Pro users can still create, refresh, and share reports without the need for a specific Fabric capacity to be assigned to their workspaces because they are operating in <strong>shared capacity</strong> by default.</p>
<p>However, even though these users are not directly connected to a Fabric capacity, <strong>Power BI Pro users can still access semantic models created in the F2-F32 Fabric capacity</strong>, provided a few conditions are met:</p>
<ul>
<li><p><strong>Same tenant</strong>: The semantic models and reports must be within the same tenant. Tenants serve as the overarching boundaries within which data and resources are shared.</p>
</li>
<li><p><strong>Permissions</strong>: Ensure the Pro users have <strong>Build</strong> or <strong>Read</strong> permissions on the dataset hosting the semantic model. This gives them the ability to connect to and use the models in their reports.</p>
</li>
<li><p><strong>Workspace settings</strong>: The workspace containing the semantic models must allow access from shared or external workspaces for cross-capacity interaction.</p>
</li>
</ul>
<p>This setup allows organizations to maximize their Fabric capacity while enabling Pro users to work efficiently. However, keep in mind that <strong>free users</strong> (those without Power BI Pro licenses) won’t be able to refresh or interact with the data unless the reports are hosted in a <strong>Premium Fabric capacity</strong>, which supports free-user access. In shared capacity, they would be restricted to viewing content only if it’s published to them through other means.</p>
<hr />
<h3 id="heading-q2-with-a-user-base-of-1000-employees-what-is-the-best-way-to-manage-power-bi-licenses-should-i-assign-power-bi-pro-licenses-to-each-user-or-would-it-make-more-sense-to-use-a-larger-fabric-capacity-like-f64"><strong>Q2: With a user base of 1000+ employees, what is the best way to manage Power BI licenses? Should I assign Power BI Pro licenses to each user, or would it make more sense to use a larger Fabric capacity like F64?</strong></h3>
<p><strong>A: For large-scale environments, using a Premium Fabric capacity like F64 is a more scalable and cost-effective solution.</strong></p>
<p>Managing Power BI Pro licenses for over 1000 users is not only expensive but also introduces administrative complexities. Let’s explore why transitioning to an F64 capacity makes more sense in this scenario:</p>
<ol>
<li><p><strong>Cost-efficiency</strong>: Assigning individual Power BI Pro licenses for 1000+ users can lead to significant costs over time. Additionally, with each Pro user relying on shared capacity for refreshing and hosting reports, you may encounter performance bottlenecks as the system struggles to handle the load of a large user base. Free users, in particular, are heavily limited under shared capacity as they won’t be able to refresh or interact with data unless it’s published in a Premium workspace.</p>
</li>
<li><p><strong>Scalability with F64</strong>: By shifting to an <strong>F64 Premium Fabric capacity</strong>, you gain the advantage of:</p>
<ul>
<li><p><strong>Unlimited free-user access</strong>: Free users can view reports in workspaces tied to Premium capacity without needing individual licenses, drastically cutting down licensing costs.</p>
</li>
<li><p><strong>Performance</strong>: Dedicated resources in the F64 capacity ensure reports are generated and refreshed efficiently, even under heavy workloads. This is critical when you’re dealing with complex semantic models or high user traffic.</p>
</li>
<li><p><strong>Centralized governance</strong>: Hosting all reports in a dedicated Premium workspace simplifies report management and governance. You gain better control over refresh schedules, security, and performance.</p>
</li>
</ul>
</li>
<li><p><strong>Ideal for enterprise environments</strong>: For large user bases like yours, transitioning to F64 or higher capacity supports enterprise-scale operations, making it easier to manage thousands of users while ensuring smooth performance across the board.</p>
</li>
</ol>
<p>For smaller teams (eg: fewer than 100 users) or less complex use cases, assigning Power BI Pro licenses might be a viable, cost-effective option. However, as user numbers increase or if workloads become more demanding, moving to a <strong>Premium Fabric capacity</strong> is the logical step for both performance and cost management.</p>
<hr />
<h3 id="heading-q3-what-exactly-is-power-bi-shared-capacity-and-can-any-user-view-reports-hosted-in-shared-capacity"><strong>Q3: What exactly is Power BI shared capacity, and can any user view reports hosted in shared capacity?</strong></h3>
<p><strong>A: Shared capacity refers to the default resource pool that Power BI provides to Pro users who are not assigned to a dedicated Premium capacity.</strong></p>
<p>Let’s unpack this further:</p>
<ul>
<li><p><strong>Shared capacity</strong>: This is a multi-tenant environment where resources (such as CPU, memory, and storage) are pooled and shared across multiple users and organizations. It’s the default mode of operation for Power BI workspaces that don’t have dedicated Fabric or Premium capacity assigned to them.</p>
</li>
<li><p><strong>Access limitations</strong>: Only <strong>Power BI Pro users</strong> can create, view, and interact with reports hosted in shared capacity. These users must be explicitly granted permissions (such as workspace member or app access) to access reports. <strong>Free users</strong> cannot view or refresh reports in shared capacity unless the reports are published in a workspace backed by a <strong>Premium Fabric capacity</strong>.</p>
</li>
<li><p><strong>Performance considerations</strong>: While shared capacity works well for smaller teams or simpler workloads, it has its limitations:</p>
<ul>
<li><p><strong>Resource constraints</strong>: Since resources are shared across many users, performance can degrade during peak usage periods, especially when handling large datasets or complex queries.</p>
</li>
<li><p><strong>Limited refreshes</strong>: Shared capacity allows up to 8 dataset refreshes per day, which may not be sufficient for real-time or frequent data updates.</p>
</li>
</ul>
</li>
</ul>
<p>To unlock free-user access, better performance, and more refreshes, transitioning to <strong>Premium Fabric capacity</strong> is a smart move.</p>
<hr />
<h3 id="heading-q4-is-it-true-that-power-bi-pro-users-can-access-semantic-models-created-in-another-fabric-capacity-within-the-same-tenant-even-though-they-arent-assigned-to-that-specific-capacity-how-does-this-cross-capacity-access-work"><strong>Q4: Is it true that Power BI Pro users can access semantic models created in another Fabric capacity within the same tenant, even though they aren’t assigned to that specific capacity? How does this cross-capacity access work?</strong></h3>
<p><strong>A: Yes, Power BI Pro users can access semantic models in a different Fabric capacity within the same tenant, as long as the right permissions are set.</strong></p>
<p>Here’s how cross-capacity access works:</p>
<ul>
<li><p><strong>Tenant structure</strong>: A tenant serves as the boundary that governs all Power BI resources within an organization. Different Fabric capacities, like F2 or F64, can exist within the same tenant, and resources such as datasets or semantic models can be shared across them, provided permissions are correctly configured.</p>
</li>
<li><p><strong>Permissions</strong>: To access a semantic model hosted in another Fabric capacity, Pro users need <strong>Build</strong> or <strong>Read</strong> permissions on that dataset. These permissions allow users to connect to the model via <strong>DirectQuery</strong> or <strong>Live Connection</strong>, using the model in reports hosted in their own workspaces.</p>
</li>
<li><p><strong>Capacity isolation with tenant interaction</strong>: While each Fabric capacity provides isolated computational resources, they are not entirely “separate territories.” Data remains within the tenant boundary, and cross-capacity access is permitted through tenant-level security and permissions. The capacity hosting the semantic model handles the computation, while the user’s report runs in their own capacity (shared or Premium).</p>
</li>
</ul>
<p>This flexibility allows organizations to leverage resources across multiple capacities while maintaining security and performance standards.</p>
<hr />
<h3 id="heading-q5-should-i-continue-using-shared-capacity-or-is-it-time-to-upgrade-to-a-premium-fabric-capacity-for-my-reporting-needs"><strong>Q5: Should I continue using shared capacity, or is it time to upgrade to a Premium Fabric capacity for my reporting needs?</strong></h3>
<p><strong>A: It depends on your scale and performance requirements. Here’s a comparison to help you decide:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Shared Capacity</strong></td><td><strong>Premium Fabric Capacity (F64)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Free-user access</strong></td><td>Not supported</td><td>Supported (free users can view reports)</td></tr>
<tr>
<td><strong>Performance</strong></td><td>Shared resources, performance may degrade</td><td>Dedicated, scalable resources</td></tr>
<tr>
<td><strong>Refresh rate</strong></td><td>8 refreshes/day</td><td>48 refreshes/day (or more)</td></tr>
<tr>
<td><strong>Report sharing</strong></td><td>Only with Pro users</td><td>Pro and free users</td></tr>
<tr>
<td><strong>Ideal for</strong></td><td>Small teams with light workloads</td><td>Large teams or complex, high-demand reports</td></tr>
</tbody>
</table>
</div><p><strong>Shared capacity</strong> is suitable for small teams or simpler reporting needs where only <strong>Power BI Pro users</strong> need to access reports. However, as your organization scales and you require more performance and access flexibility, moving to a <strong>Premium Fabric capacity</strong> (like F64) is a wise investment. It ensures smoother operations, supports more refreshes, and grants free user access, reducing the need for costly individual Pro licenses.</p>
<hr />
<h3 id="heading-conclusion"><strong>Conclusion:</strong></h3>
<p>Managing your Microsoft Fabric capacity and Power BI licensing strategy is crucial for optimizing performance, ensuring scalability, and managing costs as your organization grows. For organizations with large user bases or complex reporting needs, moving to a <strong>Premium Fabric capacity</strong> like F64 is the best choice(yet it depends on the work load 😌), as it provides dedicated resources, supports free-user access, and simplifies report management.</p>
<p>By carefully considering your organization’s needs—whether you’re a small team working in shared capacity or a large enterprise needing the scalability of Premium Fabric Capacity—you can ensure that your Power BI environment is future-proofed and ready to handle the demands of modern data management.</p>
<p><strong>Thanks For Reading !!!</strong> 😊</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Understanding the Difference: "Wait on Completion" vs. "Parallel Execution" in Microsoft Fabric Pipelines / ADF]]></title><description><![CDATA[When designing pipelines in Microsoft Fabric or Azure Data Factory (ADF), understanding the execution flow of activities is critical. One setting that often causes confusion is "Wait on Completion"—especially in how it differs from parallel execution...]]></description><link>https://bidiaries.com/understanding-the-difference-wait-on-completion-vs-parallel-execution-in-microsoft-fabric-pipelines-adf</link><guid isPermaLink="true">https://bidiaries.com/understanding-the-difference-wait-on-completion-vs-parallel-execution-in-microsoft-fabric-pipelines-adf</guid><category><![CDATA[ADF]]></category><category><![CDATA[Azure Data Factory]]></category><category><![CDATA[fabric]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Pipeline]]></category><category><![CDATA[Azure Pipelines]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[#microsoft-azure]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[Power BI]]></category><dc:creator><![CDATA[Nalaka Wanniarachchi]]></dc:creator><pubDate>Sun, 24 Nov 2024 05:39:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Bt0PM7cNJFQ/upload/2f9bac0a82772414063d13dd24d1d9da.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>When designing pipelines in <strong>Microsoft Fabric</strong> or <strong>Azure Data Factory (ADF)</strong>, understanding the execution flow of activities is critical. One setting that often causes confusion is <strong>"Wait on Completion"</strong>—especially in how it differs from <strong>parallel execution</strong>.</p>
<p>In this post, I’ll explore these concepts, highlight their differences with examples and visuals, and explain how to choose the right execution approach for your pipeline.</p>
<hr />
<h3 id="heading-what-is-wait-on-completion"><strong>What is "Wait on Completion"?</strong></h3>
<p>The <strong>"Wait on Completion"</strong> setting controls whether a pipeline waits for an activity to <strong>finish</strong> before triggering the next activity.</p>
<ul>
<li><p><strong>Enabled (Default):</strong> The pipeline triggers Activity A and waits for it to <strong>complete</strong> before moving on to Activity B.</p>
</li>
<li><p><strong>Disabled:</strong> The pipeline triggers Activity A and then <strong>immediately moves to Activity B</strong> without waiting for Activity A to complete.</p>
</li>
</ul>
<p>Here’s a quick summary of how this works:</p>
<h4 id="heading-with-wait-on-completion-enabled"><strong>With "Wait on Completion" Enabled</strong></h4>
<ul>
<li><p>Activity B begins <strong>only after Activity A has completed.</strong></p>
</li>
<li><p>Ideal for tasks where Activity B relies on the result of Activity A.</p>
</li>
</ul>
<h4 id="heading-with-wait-on-completion-disabled"><strong>With "Wait on Completion" Disabled</strong></h4>
<ul>
<li><p><mark>Activity B starts </mark> <strong><mark>as soon as Activity A is triggered,</mark></strong> <mark>even if Activity A is still running.</mark></p>
</li>
<li><p>Useful for tasks where Activity B does not depend on the outcome of Activity A.</p>
</li>
</ul>
<hr />
<h3 id="heading-is-it-the-same-as-parallel-execution"><strong>Is It the Same as Parallel Execution?</strong></h3>
<p>At first glance, disabling <strong>"Wait on Completion"</strong> may seem like creating parallel execution. However, <strong>it’s not the same.</strong> Let’s clarify the difference:</p>
<h4 id="heading-asynchronous-execution-wait-disabled"><strong>Asynchronous Execution (Wait Disabled)</strong></h4>
<ul>
<li><p>Activity B starts as soon as Activity A is triggered.</p>
</li>
<li><p>There is still a sequential dependency: Activity B won’t start until Activity A is <strong>triggered</strong>.</p>
</li>
<li><p>Their execution overlaps, but they are not truly independent.</p>
</li>
</ul>
<h4 id="heading-parallel-execution"><strong>Parallel Execution</strong></h4>
<ul>
<li><p>Activities are configured as <strong>independent branches</strong> in the pipeline.</p>
</li>
<li><p>Both activities start at the same time, with no dependency or sequence between them.</p>
</li>
<li><p>True parallel execution must be explicitly defined in the pipeline design.</p>
</li>
</ul>
<hr />
<h3 id="heading-visual-comparison"><strong>Visual Comparison</strong></h3>
<p>To illustrate this, let’s consider two activities:</p>
<ul>
<li><p><strong>Activity A:</strong> Sending an email.</p>
</li>
<li><p><strong>Activity B:</strong> Logging the email status in a database.</p>
</li>
</ul>
<h4 id="heading-with-wait-on-completion-enabled-1"><strong>With "Wait on Completion" Enabled</strong></h4>
<p>Activity B waits until Activity A is complete before starting.</p>
<pre><code class="lang-plaintext">|--- Activity A ---| (email sent)
                   |--- Activity B ---| (log written)
</code></pre>
<h4 id="heading-with-wait-on-completion-disabled-1"><strong>With "Wait on Completion" Disabled</strong></h4>
<p>Activity B starts as soon as Activity A is triggered, even while Activity A is still running.</p>
<pre><code class="lang-plaintext">|--- Activity A ---| (email in progress)
    |--- Activity B ---| (log written)
</code></pre>
<h4 id="heading-parallel-execution-1"><strong>Parallel Execution</strong></h4>
<p>Both activities start at the same time and run independently.</p>
<pre><code class="lang-plaintext">|--- Activity A ---| (email sent)
|--- Activity B ---| (log written)
</code></pre>
<hr />
<h3 id="heading-use-cases-for-each-approach"><strong>Use Cases for Each Approach</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Scenario</strong></td><td><strong>Asynchronous Execution</strong> (Wait Disabled)</td><td><strong>Parallel Execution</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Dependency Exists?</strong></td><td>Yes, Activity B <mark>depends</mark> on A being triggered.</td><td>No, tasks are fully independent.</td></tr>
<tr>
<td><strong>Execution Overlaps?</strong></td><td>Yes, but A is triggered before B.</td><td>Yes, both start simultaneously.</td></tr>
<tr>
<td><strong>Use Case Examples</strong></td><td>Logging, notifications, background tasks.</td><td>Independent data processing.</td></tr>
</tbody>
</table>
</div><hr />
<h3 id="heading-why-the-difference-matters"><strong>Why the Difference Matters</strong></h3>
<p>Disabling <strong>"Wait on Completion"</strong> introduces an <strong>execution overlap</strong> that speeds up sequential pipelines by eliminating unnecessary wait times. However, it still ensures a logical order: Activity A must trigger before Activity B starts. This contrasts with <strong>true parallel execution</strong>, where activities run independently and simultaneously, without triggering dependencies.</p>
<hr />
<h3 id="heading-how-to-decide-which-to-use"><strong>How to Decide Which to Use</strong></h3>
<ol>
<li><p><strong>Use "Wait on Completion" (Enabled):</strong></p>
<ul>
<li><p>When the next activity depends on the completion of the current activity.</p>
</li>
<li><p>Example: Loading data from a source and then transforming it.</p>
</li>
</ul>
</li>
<li><p><strong>Disable "Wait on Completion" (Asynchronous Execution):</strong></p>
<ul>
<li><p>When tasks can overlap but still require a sequence of triggers.</p>
</li>
<li><p>Example: Sending notifications and logging them simultaneously.</p>
</li>
</ul>
</li>
<li><p><strong>Use Parallel Execution:</strong></p>
<ul>
<li><p>When tasks are entirely independent and can run at the same time.</p>
</li>
<li><p>Example: Processing different datasets concurrently.</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-tips"><strong>Tips</strong></h3>
<p>In a nutshell when <strong>"Wait on Completion"</strong> is <strong>disabled</strong> without explicitly setting up parallel activities, the <strong>triggers</strong> are still <strong>sequential</strong> (Activity A starts first, and then Activity B is triggered immediately after). However, the <strong>execution</strong> of the activities can happen <strong>in parallel</strong> (they can overlap), since Activity B doesn't wait for Activity A to finish before starting.</p>
<p>So, the activities are <strong><mark>triggered sequentially</mark></strong> <mark>but </mark> <strong><mark>executed concurrently</mark></strong> (parallel execution) as soon as they are triggered.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732426081298/a3409a52-74ad-4788-8893-42ffebb49f0b.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-can-you-tell-just-by-looking-at-the-pipeline-layout"><strong>Can You Tell Just By Looking at the Pipeline Layout?</strong></h3>
<p>Here’s an important thing to note: Just by glancing at the layout of activities in the <strong>pipeline canvas</strong>, you can't always immediately tell if the execution is <strong>sequential</strong> or <strong>parallel</strong>. The <strong>visual layout</strong> might make it look like activities are in sequence or parallel, but the actual behaviour depends on configuration settings like <strong>"Wait on Completion"</strong>.</p>
<ul>
<li><p><strong>Sequential Triggering:</strong> Activities can appear sequential in a straight line. But if <strong>"Wait on Completion"</strong> is disabled, they can still run <strong>asynchronously</strong> (overlap in execution).</p>
</li>
<li><p><strong>Parallel Execution:</strong> Activities in <strong>multiple branches</strong> might seem like they run in parallel, but for them to truly execute in parallel, they must be set up as independent tasks with no dependencies or <strong>explicit parallel configuration</strong>.</p>
</li>
</ul>
<p>So, the <strong>layout of activities</strong> alone doesn’t tell the whole story. It’s important to check how activities are configured in terms of triggers, dependencies, and the <strong>"Wait on Completion"</strong> setting to fully understand how they will execute.</p>
<hr />
<h3 id="heading-final-thoughts"><strong>Final Thoughts</strong></h3>
<p>Choosing between <strong>"Wait on Completion"</strong> and <strong>parallel execution</strong> depends on your pipeline’s requirements. By understanding their differences, you can design pipelines that are both efficient and reliable. Microsoft Fabric gives you the flexibility to optimize execution for various scenarios, so experiment with these settings to find what works best for your projects!</p>
<p>Have questions or your own tips to share? Leave a comment below—I’d love to hear your thoughts!</p>
<p><strong>Thanks for Reading !!! ❤️</strong></p>
<hr />
]]></content:encoded></item></channel></rss>