Skip to content

Deployment

Deployment engine shared by the deploy_all_* helpers.

A run plans first, then applies. It selects the local items (every item, or only those changed in Git since a baseline commit, which a deployment state can supply per item type), lists the workspace items and folders once, and builds a DeploymentPlan from them without changing anything. DeploymentExecutor then applies the plan, and the run returns a DeploymentReport with the outcome of each item, so a partial failure reaches the caller instead of being logged and lost. The state is recorded only when every item succeeded. An item deleted from Git is deleted from the workspace only when the run allows deletions, after every other item succeeded, and only while no item that stays in the source refers to it.

DeploymentExecutor

Apply a deployment plan to a workspace through the Fabric API.

The executor decides nothing: it creates the items planned as CREATE, updates the items planned as UPDATE (moving them first when their folder differs), moves the items planned as MOVE without sending their definition, deletes the items planned as DELETE, and reports BLOCKED items as failed, creating missing folders on the way. A NOOP gets a log line but no result. An item to create, update or move is skipped when an item of its needs failed or was skipped, so nothing is deployed without what it needs. A deletion is refused, and reported as failed, unless allow_deletions is set; it is skipped when any action before it failed or was skipped. A report that points to its semantic model by path is sent with a connection to the model's ID in the workspace instead, since the Fabric API accepts no path. Used by deploy_all_items; not exported from pyfabricops yet.

Parameters:

Name Type Description Default
index _WorkspaceIndex

The workspace items and folders the plan was built from.

required
fail_fast bool

Stop at the first failed action and report the remaining ones as skipped. Defaults to False.

False
allow_deletions bool

Delete the items planned as DELETE. Defaults to False: each is reported as failed and left in the workspace.

False
on_result Callable

Called with each action and its result as soon as the result is known, such as to write a journal. Defaults to none.

None
Source code in src/pyfabricops/helpers/deployment.py
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
class DeploymentExecutor:
    """
    Apply a deployment plan to a workspace through the Fabric API.

    The executor decides nothing: it creates the items planned as CREATE,
    updates the items planned as UPDATE (moving them first when their folder
    differs), moves the items planned as MOVE without sending their
    definition, deletes the items planned as DELETE, and reports BLOCKED
    items as failed, creating missing folders on the way. A NOOP gets a log
    line but no result. An item to create, update or move is skipped when
    an item of its ``needs`` failed or was skipped, so nothing is deployed
    without what it needs. A deletion is refused, and reported as failed,
    unless ``allow_deletions`` is set; it is skipped when any action before
    it failed or was skipped. A report that points to its semantic model by
    path is sent with a connection to the model's ID in the workspace
    instead, since the Fabric API accepts no path. Used by
    ``deploy_all_items``; not exported from ``pyfabricops`` yet.

    Args:
        index (_WorkspaceIndex): The workspace items and folders the plan was
            built from.
        fail_fast (bool, optional): Stop at the first failed action and
            report the remaining ones as skipped. Defaults to False.
        allow_deletions (bool, optional): Delete the items planned as
            DELETE. Defaults to False: each is reported as failed and left
            in the workspace.
        on_result (Callable, optional): Called with each action and its
            result as soon as the result is known, such as to write a
            journal. Defaults to none.
    """

    def __init__(
        self,
        index: _WorkspaceIndex,
        *,
        fail_fast: bool = False,
        allow_deletions: bool = False,
        on_result: Callable[[DeploymentAction, DeploymentResult], None]
        | None = None,
    ) -> None:
        self._index = index
        self._fail_fast = fail_fast
        self._allow_deletions = allow_deletions
        self._on_result = on_result

    def apply(self, plan: DeploymentPlan) -> list[DeploymentResult]:
        """
        Execute the actions of a plan, in order.

        Args:
            plan (DeploymentPlan): The plan to execute.

        Returns:
            list[DeploymentResult]: One result per action other than NOOP,
                in plan order.
        """
        results: list[DeploymentResult] = []
        # Each item of the run that was not deployed, as a detail for what
        # needs it: "Name.Type, which failed".
        not_deployed: dict[tuple[str, str], str] = {}
        # Whether every action so far succeeded, which a deletion needs.
        clean = True
        for position, action in enumerate(plan.actions):
            if action.action is DeploymentActionType.NOOP:
                _log_noop(action)
                continue

            unmet = [key for key in action.needs if key in not_deployed]
            if unmet and action.action in _APPLIED:
                result = _action_result(
                    action, "skipped", error=f"Needs {not_deployed[unmet[0]]}."
                )
            elif action.action is DeploymentActionType.DELETE and not clean:
                result = _action_result(
                    action,
                    "skipped",
                    error="Not deleted: an item before it failed or was "
                    "skipped.",
                )
            else:
                result = _apply_action(
                    self._index,
                    action,
                    allow_deletions=self._allow_deletions,
                )
            results.append(result)
            _log_result(result)
            self._tell(action, result)
            which = _NOT_DEPLOYED.get(result.action)
            if which is not None:
                clean = False
                if action.display_name:
                    not_deployed[_identity(action)] = (
                        f"{_label(result)}, which {which}"
                    )

            if result.action == "failed" and self._fail_fast:
                for skipped in plan.actions[position + 1 :]:
                    if skipped.action is not DeploymentActionType.NOOP:
                        left = _action_result(skipped, "skipped")
                        results.append(left)
                        self._tell(skipped, left)
                break
        return results

    def _tell(
        self, action: DeploymentAction, result: DeploymentResult
    ) -> None:
        """Pass a result on, to whoever asked to know."""
        if self._on_result is not None:
            self._on_result(action, result)

apply(plan)

Execute the actions of a plan, in order.

Parameters:

Name Type Description Default
plan DeploymentPlan

The plan to execute.

required

Returns:

Type Description
list[DeploymentResult]

list[DeploymentResult]: One result per action other than NOOP, in plan order.

Source code in src/pyfabricops/helpers/deployment.py
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
def apply(self, plan: DeploymentPlan) -> list[DeploymentResult]:
    """
    Execute the actions of a plan, in order.

    Args:
        plan (DeploymentPlan): The plan to execute.

    Returns:
        list[DeploymentResult]: One result per action other than NOOP,
            in plan order.
    """
    results: list[DeploymentResult] = []
    # Each item of the run that was not deployed, as a detail for what
    # needs it: "Name.Type, which failed".
    not_deployed: dict[tuple[str, str], str] = {}
    # Whether every action so far succeeded, which a deletion needs.
    clean = True
    for position, action in enumerate(plan.actions):
        if action.action is DeploymentActionType.NOOP:
            _log_noop(action)
            continue

        unmet = [key for key in action.needs if key in not_deployed]
        if unmet and action.action in _APPLIED:
            result = _action_result(
                action, "skipped", error=f"Needs {not_deployed[unmet[0]]}."
            )
        elif action.action is DeploymentActionType.DELETE and not clean:
            result = _action_result(
                action,
                "skipped",
                error="Not deleted: an item before it failed or was "
                "skipped.",
            )
        else:
            result = _apply_action(
                self._index,
                action,
                allow_deletions=self._allow_deletions,
            )
        results.append(result)
        _log_result(result)
        self._tell(action, result)
        which = _NOT_DEPLOYED.get(result.action)
        if which is not None:
            clean = False
            if action.display_name:
                not_deployed[_identity(action)] = (
                    f"{_label(result)}, which {which}"
                )

        if result.action == "failed" and self._fail_fast:
            for skipped in plan.actions[position + 1 :]:
                if skipped.action is not DeploymentActionType.NOOP:
                    left = _action_result(skipped, "skipped")
                    results.append(left)
                    self._tell(skipped, left)
            break
    return results

DeploymentReport dataclass

Per-item outcome of a deploy_all_* run.

Attributes:

Name Type Description
workspace str

The workspace name or ID the run targeted.

workspace_id str | None

The resolved workspace ID.

results list[DeploymentResult]

One result per item acted on, in deployment order. An item that needs nothing has none.

Examples:

report = deploy_all_items('Sales-PRD', 'stg/workspace')
if report.failed:
    raise SystemExit(1)
report.durations_by_type()
Source code in src/pyfabricops/helpers/deployment.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
@dataclass
class DeploymentReport:
    """
    Per-item outcome of a ``deploy_all_*`` run.

    Attributes:
        workspace (str): The workspace name or ID the run targeted.
        workspace_id (str | None): The resolved workspace ID.
        results (list[DeploymentResult]): One result per item acted on, in
            deployment order. An item that needs nothing has none.

    Examples:
        ```python
        report = deploy_all_items('Sales-PRD', 'stg/workspace')
        if report.failed:
            raise SystemExit(1)
        report.durations_by_type()
        ```
    """

    workspace: str
    workspace_id: str | None = None
    results: list[DeploymentResult] = field(default_factory=list)

    @property
    def failed(self) -> list[DeploymentResult]:
        """The items that failed."""
        return [r for r in self.results if r.action == "failed"]

    @property
    def skipped(self) -> list[DeploymentResult]:
        """
        The items not attempted.

        An item they need was not deployed, or an earlier item failed with
        ``fail_fast``.
        """
        return [r for r in self.results if r.action == "skipped"]

    @property
    def ok(self) -> bool:
        """True when every item was created, updated, moved or deleted."""
        return all(
            r.action in ("created", "updated", "moved", "deleted")
            for r in self.results
        )

    @property
    def duration_seconds(self) -> float:
        """Total wall-clock time spent on the items."""
        return sum(r.duration_seconds for r in self.results)

    def summary(self) -> dict[str, int]:
        """
        Count the items by action.

        Returns:
            dict[str, int]: The number of items created, updated, moved,
                deleted, failed and skipped.
        """
        counts: dict[str, int] = {action: 0 for action in _ACTIONS}
        for result in self.results:
            counts[result.action] += 1
        return counts

    def describe(self) -> str:
        """
        Describe the run: one line per item, then the counts and the time.

        Returns:
            str: Such as ``updated  Orders.Notebook  (2.1s)``, a failed or
                skipped item followed by why, and a last line of counts.

        Examples:
            ```python
            report = deploy_all_items('Sales-PRD', staging)
            print(report.describe())
            ```
        """
        lines = []
        for result in self.results:
            line = f"{result.action:<8} {_label(result)}"
            if result.error:
                line += f": {result.error}"
            elif result.duration_seconds:
                line += f"  ({result.duration_seconds:.1f}s)"
            lines.append(line)
        counts = self.summary()
        lines.append(
            ", ".join(f"{counts[action]} {action}" for action in _ACTIONS)
            + f" in {self.duration_seconds:.1f}s"
        )
        return "\n".join(lines)

    def durations_by_type(self) -> dict[str, float]:
        """
        Sum the wall-clock time spent per item type.

        Returns:
            dict[str, float]: Seconds per item type, in deployment order.
        """
        durations: dict[str, float] = {}
        for result in self.results:
            durations[result.item_type] = (
                durations.get(result.item_type, 0.0) + result.duration_seconds
            )
        return durations

    def to_df(self) -> DataFrame:
        """
        Return the results as a DataFrame.

        Returns:
            DataFrame: One row per item, one column per result attribute.
        """
        columns = [f.name for f in fields(DeploymentResult)]
        return DataFrame([asdict(r) for r in self.results], columns=columns)

duration_seconds property

Total wall-clock time spent on the items.

failed property

The items that failed.

ok property

True when every item was created, updated, moved or deleted.

skipped property

The items not attempted.

An item they need was not deployed, or an earlier item failed with fail_fast.

describe()

Describe the run: one line per item, then the counts and the time.

Returns:

Name Type Description
str str

Such as updated Orders.Notebook (2.1s), a failed or skipped item followed by why, and a last line of counts.

Examples:

report = deploy_all_items('Sales-PRD', staging)
print(report.describe())
Source code in src/pyfabricops/helpers/deployment.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def describe(self) -> str:
    """
    Describe the run: one line per item, then the counts and the time.

    Returns:
        str: Such as ``updated  Orders.Notebook  (2.1s)``, a failed or
            skipped item followed by why, and a last line of counts.

    Examples:
        ```python
        report = deploy_all_items('Sales-PRD', staging)
        print(report.describe())
        ```
    """
    lines = []
    for result in self.results:
        line = f"{result.action:<8} {_label(result)}"
        if result.error:
            line += f": {result.error}"
        elif result.duration_seconds:
            line += f"  ({result.duration_seconds:.1f}s)"
        lines.append(line)
    counts = self.summary()
    lines.append(
        ", ".join(f"{counts[action]} {action}" for action in _ACTIONS)
        + f" in {self.duration_seconds:.1f}s"
    )
    return "\n".join(lines)

durations_by_type()

Sum the wall-clock time spent per item type.

Returns:

Type Description
dict[str, float]

dict[str, float]: Seconds per item type, in deployment order.

Source code in src/pyfabricops/helpers/deployment.py
267
268
269
270
271
272
273
274
275
276
277
278
279
def durations_by_type(self) -> dict[str, float]:
    """
    Sum the wall-clock time spent per item type.

    Returns:
        dict[str, float]: Seconds per item type, in deployment order.
    """
    durations: dict[str, float] = {}
    for result in self.results:
        durations[result.item_type] = (
            durations.get(result.item_type, 0.0) + result.duration_seconds
        )
    return durations

summary()

Count the items by action.

Returns:

Type Description
dict[str, int]

dict[str, int]: The number of items created, updated, moved, deleted, failed and skipped.

Source code in src/pyfabricops/helpers/deployment.py
225
226
227
228
229
230
231
232
233
234
235
236
def summary(self) -> dict[str, int]:
    """
    Count the items by action.

    Returns:
        dict[str, int]: The number of items created, updated, moved,
            deleted, failed and skipped.
    """
    counts: dict[str, int] = {action: 0 for action in _ACTIONS}
    for result in self.results:
        counts[result.action] += 1
    return counts

to_df()

Return the results as a DataFrame.

Returns:

Name Type Description
DataFrame DataFrame

One row per item, one column per result attribute.

Source code in src/pyfabricops/helpers/deployment.py
281
282
283
284
285
286
287
288
289
def to_df(self) -> DataFrame:
    """
    Return the results as a DataFrame.

    Returns:
        DataFrame: One row per item, one column per result attribute.
    """
    columns = [f.name for f in fields(DeploymentResult)]
    return DataFrame([asdict(r) for r in self.results], columns=columns)

DeploymentResult dataclass

Outcome of deploying one local item.

Attributes:

Name Type Description
item_type str

The Fabric item type, from the folder suffix.

display_name str | None

The display name from .platform, or None when it could not be read.

path str

The local item folder.

action str

"created", "updated", "moved" (only its folder changed, so its definition was not sent), "deleted", "failed", or "skipped" when the item was not attempted: an item it needs was not deployed, an earlier one failed with fail_fast=True, or, for a deletion, an earlier item was not deployed.

item_id str | None

The item ID in the workspace, when known.

moved bool

Whether the item was moved to another folder.

duration_seconds float

Wall-clock time spent on the item.

error str | None

Why the item failed, or which item it needs was not deployed.

Source code in src/pyfabricops/helpers/deployment.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
@dataclass(frozen=True)
class DeploymentResult:
    """
    Outcome of deploying one local item.

    Attributes:
        item_type (str): The Fabric item type, from the folder suffix.
        display_name (str | None): The display name from ``.platform``, or
            None when it could not be read.
        path (str): The local item folder.
        action (str): ``"created"``, ``"updated"``, ``"moved"`` (only its
            folder changed, so its definition was not sent), ``"deleted"``,
            ``"failed"``, or ``"skipped"`` when the item was not attempted:
            an item it needs was not deployed, an earlier one failed with
            ``fail_fast=True``, or, for a deletion, an earlier item was not
            deployed.
        item_id (str | None): The item ID in the workspace, when known.
        moved (bool): Whether the item was moved to another folder.
        duration_seconds (float): Wall-clock time spent on the item.
        error (str | None): Why the item failed, or which item it needs
            was not deployed.
    """

    item_type: str
    display_name: str | None
    path: str
    action: DeploymentOutcome
    item_id: str | None = None
    moved: bool = False
    duration_seconds: float = 0.0
    error: str | None = None