weaverstack 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. weaver/__init__.py +59 -0
  2. weaver/build_bundle/__init__.py +109 -0
  3. weaver/build_bundle/aliases.py +325 -0
  4. weaver/build_bundle/bundle.py +359 -0
  5. weaver/build_bundle/catalogue_actions.py +275 -0
  6. weaver/build_bundle/changes.py +186 -0
  7. weaver/build_bundle/endpoints.py +83 -0
  8. weaver/build_bundle/executors/__init__.py +69 -0
  9. weaver/build_bundle/executors/alias.py +202 -0
  10. weaver/build_bundle/executors/base.py +132 -0
  11. weaver/build_bundle/executors/folder.py +71 -0
  12. weaver/build_bundle/executors/load_file.py +205 -0
  13. weaver/build_bundle/executors/spark_case.py +26 -0
  14. weaver/build_bundle/executors/spark_schema.py +60 -0
  15. weaver/build_bundle/executors/spark_sql.py +59 -0
  16. weaver/build_bundle/executors/spark_sql_batch.py +57 -0
  17. weaver/build_bundle/executors/spark_table.py +213 -0
  18. weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
  19. weaver/build_bundle/executors/tsql.py +81 -0
  20. weaver/build_bundle/incremental.py +288 -0
  21. weaver/build_bundle/installer.py +384 -0
  22. weaver/build_bundle/models.py +288 -0
  23. weaver/build_bundle/payloads.py +34 -0
  24. weaver/build_bundle/physical.py +625 -0
  25. weaver/build_bundle/planner.py +389 -0
  26. weaver/build_bundle/prune.py +620 -0
  27. weaver/build_bundle/report.py +108 -0
  28. weaver/build_bundle/stages.py +196 -0
  29. weaver/build_bundle/targets.py +272 -0
  30. weaver/build_bundle/workflow.py +585 -0
  31. weaver/catalogue/__init__.py +73 -0
  32. weaver/catalogue/builtin.py +238 -0
  33. weaver/catalogue/claims.py +121 -0
  34. weaver/catalogue/projection.py +437 -0
  35. weaver/catalogue/reader.py +152 -0
  36. weaver/catalogue/reconcile.py +231 -0
  37. weaver/catalogue/render.py +410 -0
  38. weaver/catalogue/state.py +660 -0
  39. weaver/catalogue/tables.py +648 -0
  40. weaver/config.py +178 -0
  41. weaver/declaration/__init__.py +171 -0
  42. weaver/declaration/columns.py +223 -0
  43. weaver/declaration/ddl.py +266 -0
  44. weaver/declaration/dependencies.py +544 -0
  45. weaver/declaration/graph.py +240 -0
  46. weaver/declaration/item_dependencies.py +292 -0
  47. weaver/declaration/load.py +191 -0
  48. weaver/declaration/metadata.py +1405 -0
  49. weaver/declaration/model.py +448 -0
  50. weaver/declaration/references.py +294 -0
  51. weaver/declaration/repository.py +959 -0
  52. weaver/declaration/schemas.py +135 -0
  53. weaver/declaration/source.py +674 -0
  54. weaver/declaration/spark_load.py +759 -0
  55. weaver/declaration/sql_shaping.py +591 -0
  56. weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
  57. weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
  58. weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
  59. weaver/declaration/templates/load/column_metadata.sql +40 -0
  60. weaver/declaration/templates/load/full_replace_body.sql +21 -0
  61. weaver/declaration/templates/load/install_load_procedure.sql +27 -0
  62. weaver/declaration/templates/load/load_procedure.sql +48 -0
  63. weaver/declaration/templates/load/primary_key_body.sql +113 -0
  64. weaver/declaration/tsql_ddl.py +468 -0
  65. weaver/declaration/tsql_load.py +417 -0
  66. weaver/declaration/warehouse_type_mapping.yml +93 -0
  67. weaver/diagnostics.py +247 -0
  68. weaver/errors.py +61 -0
  69. weaver/etl.py +469 -0
  70. weaver/fabric/__init__.py +107 -0
  71. weaver/fabric/auth.py +137 -0
  72. weaver/fabric/capacity.py +143 -0
  73. weaver/fabric/client.py +147 -0
  74. weaver/fabric/environment.py +460 -0
  75. weaver/fabric/livy.py +478 -0
  76. weaver/fabric/notebooks.py +201 -0
  77. weaver/fabric/onelake.py +263 -0
  78. weaver/fabric/resolution.py +344 -0
  79. weaver/fabric/resources.py +245 -0
  80. weaver/fabric/session.py +148 -0
  81. weaver/fabric/shortcuts.py +120 -0
  82. weaver/fabric/sql.py +118 -0
  83. weaver/fabric/store.py +198 -0
  84. weaver/initialise.py +209 -0
  85. weaver/lakehouse.py +386 -0
  86. weaver/load.py +474 -0
  87. weaver/load_execution.py +483 -0
  88. weaver/load_plan.py +912 -0
  89. weaver/load_report.py +330 -0
  90. weaver/load_resolution.py +386 -0
  91. weaver/locations.py +164 -0
  92. weaver/objects.py +392 -0
  93. weaver/operations.py +757 -0
  94. weaver/physical_wipe.py +369 -0
  95. weaver/push.py +76 -0
  96. weaver/resolution.py +292 -0
  97. weaver/runtime/__init__.py +30 -0
  98. weaver/runtime/folder_load.py +402 -0
  99. weaver/runtime/load_contract.py +245 -0
  100. weaver/runtime/load_result.py +104 -0
  101. weaver/runtime/spark_load.py +152 -0
  102. weaver/runtime/table_load.py +497 -0
  103. weaver/spark/__init__.py +49 -0
  104. weaver/spark/catalogue.py +245 -0
  105. weaver/spark/destination.py +195 -0
  106. weaver/spark/session.py +84 -0
  107. weaver/spark/tokens.py +138 -0
  108. weaver/sql/__init__.py +40 -0
  109. weaver/sql/authentication.py +38 -0
  110. weaver/sql/connection.py +90 -0
  111. weaver/sql/errors.py +25 -0
  112. weaver/sql/execution.py +123 -0
  113. weaver/sql/pool.py +174 -0
  114. weaver/sql/wipe.py +156 -0
  115. weaver/store.py +209 -0
  116. weaver/targets.py +257 -0
  117. weaver/task_logging.py +215 -0
  118. weaver/unbind.py +74 -0
  119. weaver/workspaces.py +175 -0
  120. weaver_cli/__init__.py +12 -0
  121. weaver_cli/__main__.py +7 -0
  122. weaver_cli/main.py +626 -0
  123. weaverstack-0.1.1.dist-info/METADATA +113 -0
  124. weaverstack-0.1.1.dist-info/RECORD +127 -0
  125. weaverstack-0.1.1.dist-info/WHEEL +4 -0
  126. weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
  127. weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
weaver/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """The small notebook-facing Weaver interface.
2
+
3
+ Internal workspace, repository, target, storage, SQL, bundle, installer, and
4
+ control-plane composition seams live in their owning modules. Importing the
5
+ product namespace remains safe without PySpark, Fabric credentials, or the
6
+ optional desktop CLI.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from . import config as _config # establish declaration/workspace import order
12
+ from .errors import CommandError, ConfigError, IdentityError, WeaverError
13
+ from .operations import BuildResult, WipeReport, WipeResult, build, wipe
14
+ from .load import load
15
+ from .load_report import LoadMessage, LoadNodeReport, LoadResult, LoadRunReport
16
+ from .lakehouse import Lakehouse, default_lakehouse, lakehouse_for
17
+ from .objects import Folder, Table, View, WeaverObject
18
+
19
+
20
+ def _resolve_version() -> str:
21
+ """Read the git-derived installed distribution version."""
22
+
23
+ try:
24
+ from importlib.metadata import version
25
+
26
+ return version("weaverstack")
27
+ except Exception: # pragma: no cover - never worth crashing an import over
28
+ return "0.0.0+unknown"
29
+
30
+
31
+ __version__ = _resolve_version()
32
+
33
+ __all__ = [
34
+ "__version__",
35
+ # ordinary operations and results
36
+ "build",
37
+ "BuildResult",
38
+ "wipe",
39
+ "WipeReport",
40
+ "WipeResult",
41
+ "load",
42
+ "LoadRunReport",
43
+ "LoadNodeReport",
44
+ "LoadMessage",
45
+ "LoadResult",
46
+ # authored objects
47
+ "WeaverObject",
48
+ "Folder",
49
+ "Table",
50
+ "View",
51
+ "Lakehouse",
52
+ "default_lakehouse",
53
+ "lakehouse_for",
54
+ # common errors
55
+ "WeaverError",
56
+ "CommandError",
57
+ "ConfigError",
58
+ "IdentityError",
59
+ ]
@@ -0,0 +1,109 @@
1
+ """Weaver build — planning a declaration into a bundle, and installing it.
2
+
3
+ The boundary is deliberate::
4
+
5
+ WeaverRepository -> planner -> BuildBundle -> installer -> workspaces
6
+
7
+ The planner owns every decision — item selection, ordering, executable
8
+ generation, certification. The installer owns execution only: it validates a
9
+ bundle and runs it, and never reads the source declaration, resolves a
10
+ dependency or selects a target.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .bundle import (
16
+ BuildBundle,
17
+ compute_bundle_id,
18
+ load_bundle,
19
+ plan_from_yaml,
20
+ plan_to_yaml,
21
+ write_bundle,
22
+ )
23
+ from .models import (
24
+ BuildAction,
25
+ BuildBatch,
26
+ BuildPlan,
27
+ BuildSequence,
28
+ OmittedNode,
29
+ )
30
+ from .installer import InstallationEnvironment, execute_action, install_bundle
31
+ from .incremental import BuildSelection, Impact, determine_impact
32
+ from .physical import RenderedAction, render_document_build_action
33
+ from .planner import PlannedItem, generate_item_build_bundle, plan_item_build
34
+ from .report import InstallationReport
35
+ from .workflow import (
36
+ BuildState,
37
+ ItemBuildResult,
38
+ build_item_repository,
39
+ build_item_repository_source,
40
+ build_uploaded_item_repository,
41
+ catalogue_items_for_build,
42
+ install_bundle_archive,
43
+ materialise_bundle_archive,
44
+ materialise_tree,
45
+ prepare_repository,
46
+ persist_bundle_archive,
47
+ timestamped_archive_name,
48
+ read_build_state,
49
+ validate_build_request,
50
+ )
51
+ from .targets import (
52
+ BoundTarget,
53
+ LakehouseBinding,
54
+ ItemBinding,
55
+ ItemBindings,
56
+ parse_item_binding,
57
+ effective_item_bindings,
58
+ WarehouseBinding,
59
+ )
60
+
61
+ __all__ = [
62
+ "BoundTarget",
63
+ "LakehouseBinding",
64
+ "ItemBinding",
65
+ "ItemBindings",
66
+ "parse_item_binding",
67
+ "effective_item_bindings",
68
+ "WarehouseBinding",
69
+ "OmittedNode",
70
+ "BuildAction",
71
+ "BuildBatch",
72
+ "BuildSequence",
73
+ "BuildPlan",
74
+ "BuildBundle",
75
+ "Impact",
76
+ "BuildSelection",
77
+ "determine_impact",
78
+ # The three narrow seams: one document rendered, one item planned, one
79
+ # action executed. Each is the lowest layer that can answer its own
80
+ # question, so a failure localises there rather than in a whole build.
81
+ "RenderedAction",
82
+ "render_document_build_action",
83
+ "PlannedItem",
84
+ "plan_item_build",
85
+ "execute_action",
86
+ "compute_bundle_id",
87
+ "load_bundle",
88
+ "write_bundle",
89
+ "plan_to_yaml",
90
+ "plan_from_yaml",
91
+ "generate_item_build_bundle",
92
+ "InstallationEnvironment",
93
+ "install_bundle",
94
+ "InstallationReport",
95
+ "ItemBuildResult",
96
+ "BuildState",
97
+ "build_item_repository",
98
+ "build_item_repository_source",
99
+ "build_uploaded_item_repository",
100
+ "materialise_tree",
101
+ "prepare_repository",
102
+ "catalogue_items_for_build",
103
+ "read_build_state",
104
+ "validate_build_request",
105
+ "persist_bundle_archive",
106
+ "materialise_bundle_archive",
107
+ "install_bundle_archive",
108
+ "timestamped_archive_name",
109
+ ]
@@ -0,0 +1,325 @@
1
+ """Planning each repository alias as one physical action.
2
+
3
+ An alias is the consuming item's own name for something another item produces.
4
+ It is owned by its destination item — the source stays the canonical producer —
5
+ so it is planned as part of that item's work, ahead of every document the item
6
+ declares, because those documents are written against the namespace it
7
+ establishes.
8
+
9
+ What an alias *becomes* depends on the destination it is bound to, and that is a
10
+ planning decision because it is a decision about the target kind:
11
+
12
+ =============================== ==============================================
13
+ Fabric or local Lakehouse a ``create_alias`` action — a OneLake shortcut
14
+ in Fabric, a filesystem link in the emulator
15
+ Fabric Warehouse a frozen view over the bound source
16
+ =============================== ==============================================
17
+
18
+ Only the last of the two is spelled out in SQL, because only there is the
19
+ statement itself the semantic decision. A shortcut and a link are two transports
20
+ for one frozen decision — this destination, that source — which is the same split
21
+ :mod:`weaver.build_bundle.executors.spark_schema` documents for ``LOCATION``.
22
+
23
+ Where the current bindings give an alias no physical form at all, it is left out
24
+ of the plan and recorded as an omission. That is deliberately the planner's
25
+ decision and never the installer's: the installer may only run an alias action
26
+ already frozen for it.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from dataclasses import dataclass
33
+ from typing import Iterable, Mapping
34
+
35
+ from ..declaration.model import WeaverDocumentId, WeaverItemId
36
+ from .models import (
37
+ CREATE_ALIAS,
38
+ OMIT_ALIAS_UNSUPPORTED,
39
+ BuildAction,
40
+ BuildBatch,
41
+ OmittedNode,
42
+ )
43
+ from .changes import (
44
+ FOLDER as FOLDER_KIND,
45
+ TABLE as TABLE_KIND,
46
+ VIEW as VIEW_KIND,
47
+ added,
48
+ )
49
+ from .payloads import sha256_hex
50
+ from .stages import ALIAS, PlannedStage
51
+ from .targets import BoundTarget, WAREHOUSE_TARGET
52
+
53
+ #: Where a Lakehouse alias is materialised, by whether it names a Files document.
54
+ TABLES_AREA = "Tables"
55
+ FILES_AREA = "Files"
56
+
57
+
58
+ def _slug(value) -> str:
59
+ return str(value).replace("/", "--").replace(" ", "-")
60
+
61
+
62
+ def alias_node_id(destination: WeaverDocumentId) -> str:
63
+ """How an alias is named in a plan.
64
+
65
+ Prefixed, because an alias destination is not a repository document: nothing
66
+ declares it, and no dependency layer contains it.
67
+ """
68
+
69
+ return f"alias:{destination}"
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ItemAliasPlan:
74
+ """One item's planned aliases, the schemas they need, and what was left out."""
75
+
76
+ stage: PlannedStage | None = None
77
+ schemas: tuple[str, ...] = ()
78
+ omitted: tuple[OmittedNode, ...] = ()
79
+ #: The destinations behind ``omitted``, as identities rather than node ids.
80
+ #: The caller needs them because an alias with no physical form must not be
81
+ #: certified as installed — see :attr:`omitted`.
82
+ omitted_destinations: tuple[WeaverDocumentId, ...] = ()
83
+
84
+
85
+ def plan_item_aliases(
86
+ repository,
87
+ *,
88
+ item: WeaverItemId,
89
+ target: BoundTarget,
90
+ target_by_item: Mapping[WeaverItemId, BoundTarget],
91
+ selected: Iterable[WeaverDocumentId],
92
+ ) -> ItemAliasPlan:
93
+ """Plan the aliases this build selected, against the current bindings.
94
+
95
+ ``selected`` is what incremental selection chose to rebuild. An alias absent
96
+ from it is current — its declaration is unchanged, its destination is there,
97
+ and its source has not been rebuilt since — so it is left alone rather than
98
+ replaced, exactly as an unchanged document is.
99
+
100
+ Its *schema* is still reported. A retained alias lives in a namespace the
101
+ item must have, and a build that created only the schemas its rebuilt aliases
102
+ needed would leave the others homeless.
103
+ """
104
+
105
+ aliases = sorted(
106
+ (alias for alias in repository.aliases if alias.destination.item == item),
107
+ key=lambda alias: str(alias.destination),
108
+ )
109
+ if not aliases:
110
+ return ItemAliasPlan()
111
+
112
+ chosen = set(selected)
113
+ omitted: list[OmittedNode] = []
114
+ omitted_destinations: list[WeaverDocumentId] = []
115
+ supported: list[tuple] = []
116
+ schemas: list[str] = []
117
+
118
+ for alias in aliases:
119
+ source_target = target_by_item.get(alias.source.item)
120
+ reason = _unsupported(alias, target=target, source_target=source_target)
121
+ if reason is not None:
122
+ omitted.append(
123
+ OmittedNode(
124
+ node_id=alias_node_id(alias.destination),
125
+ reason=OMIT_ALIAS_UNSUPPORTED,
126
+ detail=reason,
127
+ )
128
+ )
129
+ omitted_destinations.append(alias.destination)
130
+ continue
131
+ schemas.append(alias.destination.object_id.schema)
132
+ if alias.destination in chosen:
133
+ supported.append((alias, source_target))
134
+
135
+ stage = None
136
+ if supported:
137
+ item_slug = _slug(item)
138
+ payloads: dict[str, bytes] = {}
139
+ action = _alias_action(
140
+ supported, item=item, target=target, payloads=payloads
141
+ )
142
+ # One action stands for every alias the item consumes, so it produces
143
+ # several changes. Each names what the alias physically *is* at this
144
+ # binding — a folder under Files, a view in a Warehouse, a table in a
145
+ # Lakehouse — which is the same question the Registry row answers.
146
+ stage = PlannedStage(
147
+ phase=ALIAS,
148
+ slug="item-aliases",
149
+ description="materialise item-owned aliases",
150
+ payloads=payloads,
151
+ changes={
152
+ target.id: tuple(
153
+ added(
154
+ _alias_change_kind(alias.destination, target),
155
+ alias.destination.object_id.qualified,
156
+ action.id,
157
+ )
158
+ for alias, _source_target in supported
159
+ )
160
+ },
161
+ batches=(
162
+ BuildBatch(
163
+ id=f"item-aliases-{item_slug}",
164
+ target_id=target.id,
165
+ actions=(action,),
166
+ ),
167
+ ),
168
+ )
169
+ return ItemAliasPlan(
170
+ stage=stage,
171
+ schemas=tuple(sorted(set(schemas))),
172
+ omitted=tuple(omitted),
173
+ omitted_destinations=tuple(omitted_destinations),
174
+ )
175
+
176
+
177
+ def _unsupported(alias, *, target: BoundTarget, source_target: BoundTarget | None) -> str | None:
178
+ """Why this alias has no physical form here, or None when it has one."""
179
+
180
+ if source_target is None:
181
+ return (
182
+ f"source item {alias.source.item} is not bound, so the alias has no "
183
+ "physical source to point at"
184
+ )
185
+ if alias.destination.is_files != alias.source.is_files:
186
+ return (
187
+ "an alias must stay in one namespace — a Files destination needs a "
188
+ "Files source, and a table destination a table source"
189
+ )
190
+ if target.kind != WAREHOUSE_TARGET and source_target.kind == WAREHOUSE_TARGET:
191
+ return (
192
+ "a Lakehouse alias is a OneLake shortcut, and there is no shortcut "
193
+ f"form for the Warehouse source {alias.source}"
194
+ )
195
+ return None
196
+
197
+
198
+ def _alias_change_kind(destination, target) -> str:
199
+ """What an alias will look like to an inventory, which depends on binding.
200
+
201
+ The same rule :func:`weaver.build_bundle.prune.managed_sets` applies for the
202
+ keep-set, and for the same reason: an alias is a folder, a view or a table
203
+ according to what it was bound to, and nothing about the declaration says
204
+ which.
205
+ """
206
+
207
+ if destination.is_files:
208
+ return FOLDER_KIND
209
+ return VIEW_KIND if target.kind == WAREHOUSE_TARGET else TABLE_KIND
210
+
211
+
212
+ def _alias_action(
213
+ supported,
214
+ *,
215
+ item: WeaverItemId,
216
+ target: BoundTarget,
217
+ payloads: dict[str, bytes],
218
+ ) -> BuildAction:
219
+ """One action for all of this item's aliases.
220
+
221
+ One rather than one-per-alias, because materialising an alias is not
222
+ instantaneous and the cost is per *wait*, not per create. A Lakehouse alias
223
+ becomes usable some seconds after the shortcut exists (measured at 6–31s), so
224
+ N actions run serially pay N waits while one action that creates everything and
225
+ then waits pays roughly one. A Warehouse alias is a script, and the executor
226
+ already runs a multi-statement script as one unit.
227
+
228
+ The action reports each alias it made in its details, so the manifest loses no
229
+ traceability by grouping them.
230
+ """
231
+
232
+ item_slug = _slug(item)
233
+ if target.kind == WAREHOUSE_TARGET:
234
+ content = (
235
+ json.dumps(
236
+ [
237
+ _view_statement(alias, source_target)
238
+ for alias, source_target in supported
239
+ ],
240
+ indent=2,
241
+ ensure_ascii=False,
242
+ )
243
+ + "\n"
244
+ ).encode("utf-8")
245
+ filename = f"aliases-{item_slug}.tsql-batch.json"
246
+ executor = "tsql_batch"
247
+ else:
248
+ content = _shortcut_payload(supported, target=target)
249
+ filename = f"aliases-{item_slug}.alias.json"
250
+ executor = "alias"
251
+ payloads[filename] = content
252
+ return BuildAction(
253
+ id=f"aliases-{item_slug}",
254
+ kind=CREATE_ALIAS,
255
+ # No single resource: this action stands for every alias the item
256
+ # consumes, and the payload names them.
257
+ resource_node_id=None,
258
+ executor=executor,
259
+ payload=filename,
260
+ payload_sha256=sha256_hex(content),
261
+ )
262
+
263
+
264
+ def _view_statement(alias, source_target: BoundTarget) -> str:
265
+ """A Warehouse alias, as the one statement that makes it exist.
266
+
267
+ The source is named by its bound item's three-part spelling, which is how a
268
+ Fabric Warehouse reaches another item in the same workspace, and is frozen
269
+ here for the same reason an authored three-part reference is: it is the
270
+ semantic decision, not transport.
271
+
272
+ ``CREATE OR ALTER`` rather than a drop and a create. An alias holds no data —
273
+ it is a pointer — so replacing one is not a destructive transition needing
274
+ proof of prior state, and a build that could not run twice over its own
275
+ aliases would not be re-runnable at all. It is also the only *single-statement*
276
+ way to say that, and T-SQL requires ``CREATE VIEW`` to be the first statement
277
+ in its batch: ``drop …; create view …;`` in one batch is rejected outright,
278
+ which is exactly how this was found.
279
+ """
280
+
281
+ schema = _tsql_ident(alias.destination.object_id.schema)
282
+ name = _tsql_ident(alias.destination.object_id.object)
283
+ source = ".".join(
284
+ _tsql_ident(part)
285
+ for part in (
286
+ source_target.name,
287
+ alias.source.object_id.schema,
288
+ alias.source.object_id.object,
289
+ )
290
+ )
291
+ return f"create or alter view {schema}.{name} as select * from {source};"
292
+
293
+
294
+ def _shortcut_payload(supported, *, target: BoundTarget) -> bytes:
295
+ """This item's Lakehouse aliases, as the frozen pairs of addresses they stand for.
296
+
297
+ Each is named by target id rather than by resolved path: the installer already
298
+ resolves every target the plan declares through its own environment, so a
299
+ source is addressed exactly as the destination is, and a bundle carries no
300
+ path from the machine that wrote it.
301
+ """
302
+
303
+ mapping = {
304
+ "aliases": [
305
+ {
306
+ "alias": str(alias.destination),
307
+ "source": str(alias.source),
308
+ "source_target_id": source_target.id,
309
+ "area": FILES_AREA if alias.destination.is_files else TABLES_AREA,
310
+ "schema": alias.destination.object_id.schema,
311
+ "object": alias.destination.object_id.object,
312
+ "source_area": FILES_AREA if alias.source.is_files else TABLES_AREA,
313
+ "source_schema": alias.source.object_id.schema,
314
+ "source_object": alias.source.object_id.object,
315
+ }
316
+ for alias, source_target in supported
317
+ ]
318
+ }
319
+ return (json.dumps(mapping, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode(
320
+ "utf-8"
321
+ )
322
+
323
+
324
+ def _tsql_ident(name: str) -> str:
325
+ return "[" + name.replace("]", "]]") + "]"