datajunction 0.0.148__tar.gz → 0.0.150__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datajunction-0.0.148 → datajunction-0.0.150}/PKG-INFO +1 -1
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/__about__.py +1 -1
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/_internal.py +20 -4
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/deployment.py +30 -104
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/conftest.py +4 -4
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_deploy.py +30 -109
- {datajunction-0.0.148 → datajunction-0.0.150}/.coveragerc +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/.gitignore +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/.pre-commit-config.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/LICENSE.txt +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/Makefile +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/README.md +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/claude_desktop_config.example.json +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/__init__.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/_base.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/admin.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/builder.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/cli.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/client.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/compile.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/exceptions.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/mcp/__init__.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/mcp/cli.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/mcp/config.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/models.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/nodes.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/rendering.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/init_system_nodes.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/date.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/dimension_link.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/is_active.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/materialization.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/node_type.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/node_without_description.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/nodes.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/number_of_materializations.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/number_of_nodes.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/user.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/skills/datajunction.md +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/datajunction/tags.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/pyproject.toml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/setup.cfg +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/__init__.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/companies.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/companies_dim.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/contractor.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/contractors.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/us_state.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/us_states.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/avg_length_of_employment.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/avg_repair_price.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/avg_time_to_dispatch.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/date.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/date_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/dispatcher.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/dispatchers.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/hard_hat.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/hard_hat_state.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/hard_hats.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/local_hard_hats.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/municipality.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/municipality_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/municipality_municipality_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/municipality_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/national_level_agg.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/num_repair_orders.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/regional_level_agg.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/regional_repair_efficiency.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_order.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_order_details.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_order_transform.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_orders.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_orders_cube.cube.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/total_repair_cost.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/total_repair_order_discounts.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_region.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_state.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_states.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project10/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/avg_length_of_employment.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/avg_repair_price.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/avg_time_to_dispatch.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/date.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/date_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/dispatcher.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/dispatchers.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hat.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hat_state.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hats.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/local_hard_hats.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality_municipality_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/national_level_agg.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/num_repair_orders.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/regional_level_agg.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/regional_repair_efficiency.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_order.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_order_details.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_order_transform.transform.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_orders.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_orders_cube.cube.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_type.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/total_repair_cost.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/total_repair_order_discounts.metric.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_region.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_state.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_states.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/companies.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/companies_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/us_state.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/us_states.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project2/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project2/some_node.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project3/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project3/some_node.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project4/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project4/very/very/deeply/nested/namespace/some_node.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project5/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project5/some_node.a.b.c.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project6/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project6/roads/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project6/roads/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project7/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project7/roads/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project7/roads/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project8/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/dj.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/companies.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/companies_dim.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/contractor.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/contractors.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/us_state.dimension.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/us_states.source.yaml +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/examples.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/mcp/README.md +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/mcp/__init__.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/mcp/test_cli.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test__internal.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_admin.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_base.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_builder.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_cli.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_client.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_compile.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_generated_client.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_integration.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tests/test_models.py +0 -0
- {datajunction-0.0.148 → datajunction-0.0.150}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datajunction
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.150
|
|
4
4
|
Summary: DataJunction client library for connecting to a DataJunction server
|
|
5
5
|
Project-URL: repository, https://github.com/DataJunction/dj
|
|
6
6
|
Author-email: DataJunction Authors <yian.shang@gmail.com>
|
|
@@ -683,12 +683,28 @@ class DJClient:
|
|
|
683
683
|
response = self._session.get(f"/namespaces/{namespace}/export/")
|
|
684
684
|
return response.json()
|
|
685
685
|
|
|
686
|
-
def
|
|
686
|
+
def _export_namespace_yaml_zip(
|
|
687
|
+
self,
|
|
688
|
+
namespace: str,
|
|
689
|
+
existing_zip_bytes: Optional[bytes] = None,
|
|
690
|
+
) -> bytes:
|
|
687
691
|
"""
|
|
688
|
-
Export a
|
|
692
|
+
Export a namespace as a ZIP of YAML files.
|
|
693
|
+
|
|
694
|
+
If `existing_zip_bytes` is provided, the server merges new content into
|
|
695
|
+
those files, preserving key ordering and comments. Otherwise it falls back
|
|
696
|
+
to the configured git branch or produces a fresh export.
|
|
689
697
|
"""
|
|
690
|
-
|
|
691
|
-
|
|
698
|
+
files = (
|
|
699
|
+
{"existing_zip": ("existing.zip", existing_zip_bytes, "application/zip")}
|
|
700
|
+
if existing_zip_bytes is not None
|
|
701
|
+
else None
|
|
702
|
+
)
|
|
703
|
+
response = self._session.post(
|
|
704
|
+
f"/namespaces/{namespace}/export/yaml",
|
|
705
|
+
files=files,
|
|
706
|
+
)
|
|
707
|
+
return response.content
|
|
692
708
|
|
|
693
709
|
#
|
|
694
710
|
# Methods for Tags
|
|
@@ -56,123 +56,49 @@ class DeploymentService:
|
|
|
56
56
|
self.client = client
|
|
57
57
|
self.console = console or Console()
|
|
58
58
|
|
|
59
|
-
@staticmethod
|
|
60
|
-
def clean_dict(d: dict) -> dict:
|
|
61
|
-
"""
|
|
62
|
-
Recursively remove None, empty list, and empty dict values.
|
|
63
|
-
"""
|
|
64
|
-
result = {}
|
|
65
|
-
for k, v in d.items():
|
|
66
|
-
if v is None:
|
|
67
|
-
continue
|
|
68
|
-
if isinstance(v, (list, dict)) and not v:
|
|
69
|
-
continue
|
|
70
|
-
if isinstance(v, dict):
|
|
71
|
-
nested = DeploymentService.clean_dict(v)
|
|
72
|
-
if nested: # only include if not empty after cleaning
|
|
73
|
-
result[k] = nested
|
|
74
|
-
else:
|
|
75
|
-
result[k] = v # type: ignore
|
|
76
|
-
return result
|
|
77
|
-
|
|
78
|
-
@staticmethod
|
|
79
|
-
def filter_node_for_export(node: dict) -> dict:
|
|
80
|
-
"""
|
|
81
|
-
Filter a node dict for export to YAML.
|
|
82
|
-
|
|
83
|
-
For columns:
|
|
84
|
-
- Cubes: columns are always excluded (they're inferred from metrics/dimensions)
|
|
85
|
-
- Other nodes: only includes columns with meaningful customizations
|
|
86
|
-
(display_name different from name, attributes, description, or partition).
|
|
87
|
-
Column types are excluded - let DJ infer them from the query/source.
|
|
88
|
-
"""
|
|
89
|
-
result = DeploymentService.clean_dict(node)
|
|
90
|
-
|
|
91
|
-
# Cubes should never have columns in export - they're inferred from metrics/dimensions
|
|
92
|
-
if result.get("node_type") == "cube":
|
|
93
|
-
result.pop("columns", None)
|
|
94
|
-
# For other nodes, filter columns to only include meaningful customizations
|
|
95
|
-
elif "columns" in result and result["columns"]:
|
|
96
|
-
filtered_columns = []
|
|
97
|
-
for col in result["columns"]:
|
|
98
|
-
# Check for meaningful customizations
|
|
99
|
-
has_custom_display = col.get("display_name") and col.get(
|
|
100
|
-
"display_name",
|
|
101
|
-
) != col.get("name")
|
|
102
|
-
has_attributes = bool(col.get("attributes"))
|
|
103
|
-
has_description = bool(col.get("description"))
|
|
104
|
-
has_partition = bool(col.get("partition"))
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
has_custom_display
|
|
108
|
-
or has_attributes
|
|
109
|
-
or has_description
|
|
110
|
-
or has_partition
|
|
111
|
-
):
|
|
112
|
-
# Include column but exclude type (let DJ infer)
|
|
113
|
-
filtered_col = {
|
|
114
|
-
k: v
|
|
115
|
-
for k, v in col.items()
|
|
116
|
-
if k != "type" and v # Exclude type and empty values
|
|
117
|
-
}
|
|
118
|
-
filtered_columns.append(filtered_col)
|
|
119
|
-
|
|
120
|
-
if filtered_columns:
|
|
121
|
-
result["columns"] = filtered_columns
|
|
122
|
-
else:
|
|
123
|
-
# Remove columns entirely if none have customizations
|
|
124
|
-
del result["columns"]
|
|
125
|
-
|
|
126
|
-
return result
|
|
127
|
-
|
|
128
59
|
def pull(
|
|
129
60
|
self,
|
|
130
61
|
namespace: str,
|
|
131
62
|
target_path: Union[str, Path],
|
|
132
|
-
ignore_existing_files: bool = False,
|
|
133
63
|
):
|
|
134
64
|
"""
|
|
135
65
|
Export a namespace to a local project.
|
|
66
|
+
|
|
67
|
+
Pulls the YAML files from the server's `/export/yaml` endpoint, which
|
|
68
|
+
runs every node through the same serializer (`node_spec_to_yaml`) used
|
|
69
|
+
by the UI sync-to-git flow. This way `dj pull` and a UI export produce
|
|
70
|
+
identical YAML for the same node state.
|
|
71
|
+
|
|
72
|
+
When the target directory already contains YAML files, they are uploaded
|
|
73
|
+
to the server so it can merge new content into them — preserving key
|
|
74
|
+
ordering, inline comments, and scalar styles. This means a `dj pull`
|
|
75
|
+
against an already-populated directory produces minimal diffs.
|
|
136
76
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
raise DJClientException("The target path must be empty")
|
|
140
|
-
deployment_spec = self.client._export_namespace_spec(namespace)
|
|
77
|
+
import io
|
|
78
|
+
import zipfile
|
|
141
79
|
|
|
142
|
-
namespace = deployment_spec["namespace"]
|
|
143
|
-
nodes: list[dict[str, Any]] = deployment_spec.get("nodes", [])
|
|
144
80
|
base_path = Path(target_path)
|
|
145
81
|
base_path.mkdir(parents=True, exist_ok=True)
|
|
146
82
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# Write YAML for this node (filter columns for cleaner output)
|
|
160
|
-
with open(file_path, "w") as yaml_file:
|
|
161
|
-
yaml.dump(
|
|
162
|
-
DeploymentService.filter_node_for_export(node),
|
|
163
|
-
yaml_file,
|
|
164
|
-
sort_keys=False,
|
|
165
|
-
)
|
|
83
|
+
existing_zip_bytes = None
|
|
84
|
+
existing_yaml_files = list(base_path.rglob("*.yaml"))
|
|
85
|
+
if existing_yaml_files:
|
|
86
|
+
buf = io.BytesIO()
|
|
87
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
88
|
+
for yaml_file in existing_yaml_files:
|
|
89
|
+
zf.writestr(
|
|
90
|
+
str(yaml_file.relative_to(base_path)),
|
|
91
|
+
yaml_file.read_bytes(),
|
|
92
|
+
)
|
|
93
|
+
existing_zip_bytes = buf.getvalue()
|
|
166
94
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
yaml.safe_dump(project_spec, yaml_file, sort_keys=False)
|
|
95
|
+
zip_bytes = self.client._export_namespace_yaml_zip(
|
|
96
|
+
namespace,
|
|
97
|
+
existing_zip_bytes,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
101
|
+
zf.extractall(base_path)
|
|
176
102
|
|
|
177
103
|
def push(
|
|
178
104
|
self,
|
|
@@ -192,14 +192,14 @@ def module__query_service_client(
|
|
|
192
192
|
qs_client = QueryServiceClient(uri="query_service:8001")
|
|
193
193
|
qs_client.query_state = QueryState.RUNNING # type: ignore
|
|
194
194
|
|
|
195
|
-
def mock_get_columns_for_table(
|
|
195
|
+
async def mock_get_columns_for_table(
|
|
196
196
|
catalog: str,
|
|
197
197
|
schema: str,
|
|
198
198
|
table: str,
|
|
199
|
-
engine: Optional[Engine] = None, # pylint: disable=unused-argument
|
|
200
199
|
request_headers: Optional[ # pylint: disable=unused-argument
|
|
201
200
|
Dict[str, str]
|
|
202
201
|
] = None,
|
|
202
|
+
engine: Optional[Engine] = None, # pylint: disable=unused-argument
|
|
203
203
|
) -> List[Column]:
|
|
204
204
|
return COLUMN_MAPPINGS[f"{catalog}.{schema}.{table}"]
|
|
205
205
|
|
|
@@ -209,7 +209,7 @@ def module__query_service_client(
|
|
|
209
209
|
mock_get_columns_for_table,
|
|
210
210
|
)
|
|
211
211
|
|
|
212
|
-
def mock_submit_query(
|
|
212
|
+
async def mock_submit_query(
|
|
213
213
|
query_create: QueryCreate,
|
|
214
214
|
request_headers: Optional[ # pylint: disable=unused-argument
|
|
215
215
|
Dict[str, str]
|
|
@@ -262,7 +262,7 @@ def module__query_service_client(
|
|
|
262
262
|
mock_submit_query,
|
|
263
263
|
)
|
|
264
264
|
|
|
265
|
-
def mock_create_view(
|
|
265
|
+
async def mock_create_view(
|
|
266
266
|
view_name: str,
|
|
267
267
|
query_create: QueryCreate, # pylint: disable=unused-argument
|
|
268
268
|
request_headers: Optional[ # pylint: disable=unused-argument
|
|
@@ -2,6 +2,7 @@ import importlib.metadata
|
|
|
2
2
|
import io
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import time
|
|
5
|
+
import zipfile
|
|
5
6
|
from unittest import mock
|
|
6
7
|
import pytest
|
|
7
8
|
from unittest.mock import MagicMock, patch
|
|
@@ -18,126 +19,46 @@ import yaml
|
|
|
18
19
|
from rich.console import Console
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"e": [1, 2],
|
|
28
|
-
}
|
|
29
|
-
cleaned = DeploymentService.clean_dict(dirty)
|
|
30
|
-
assert cleaned == {"d": {"k": "keep"}, "e": [1, 2]}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_filter_node_for_export_removes_columns_without_customizations():
|
|
34
|
-
"""Columns without meaningful customizations should be removed."""
|
|
35
|
-
node = {
|
|
36
|
-
"name": "test.node",
|
|
37
|
-
"query": "SELECT * FROM foo",
|
|
38
|
-
"columns": [
|
|
39
|
-
# Should be kept: has custom display_name
|
|
40
|
-
{"name": "user_id", "type": "INT", "display_name": "User ID"},
|
|
41
|
-
# Should be removed: display_name same as name
|
|
42
|
-
{"name": "created_at", "type": "TIMESTAMP", "display_name": "created_at"},
|
|
43
|
-
# Should be kept: has attributes
|
|
44
|
-
{"name": "id", "type": "BIGINT", "attributes": ["primary_key"]},
|
|
45
|
-
# Should be removed: no customizations
|
|
46
|
-
{"name": "plain_col", "type": "VARCHAR"},
|
|
47
|
-
# Should be kept: has description
|
|
48
|
-
{"name": "desc_col", "type": "TEXT", "description": "A useful column"},
|
|
49
|
-
],
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
filtered = DeploymentService.filter_node_for_export(node)
|
|
53
|
-
|
|
54
|
-
# Only columns with customizations should remain
|
|
55
|
-
assert len(filtered["columns"]) == 3
|
|
56
|
-
|
|
57
|
-
# Type should be excluded from all columns
|
|
58
|
-
for col in filtered["columns"]:
|
|
59
|
-
assert "type" not in col
|
|
60
|
-
|
|
61
|
-
# Check correct columns were kept
|
|
62
|
-
col_names = [c["name"] for c in filtered["columns"]]
|
|
63
|
-
assert "user_id" in col_names
|
|
64
|
-
assert "id" in col_names
|
|
65
|
-
assert "desc_col" in col_names
|
|
66
|
-
assert "created_at" not in col_names
|
|
67
|
-
assert "plain_col" not in col_names
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_filter_node_for_export_removes_columns_key_when_empty():
|
|
71
|
-
"""If no columns have customizations, the columns key should be removed."""
|
|
72
|
-
node = {
|
|
73
|
-
"name": "test.node",
|
|
74
|
-
"query": "SELECT * FROM foo",
|
|
75
|
-
"columns": [
|
|
76
|
-
{"name": "a", "type": "INT"},
|
|
77
|
-
{"name": "b", "type": "VARCHAR", "display_name": "b"}, # same as name
|
|
78
|
-
],
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
filtered = DeploymentService.filter_node_for_export(node)
|
|
82
|
-
|
|
83
|
-
assert "columns" not in filtered
|
|
84
|
-
|
|
22
|
+
def _make_zip(files: dict[str, str]) -> bytes:
|
|
23
|
+
buf = io.BytesIO()
|
|
24
|
+
with zipfile.ZipFile(buf, "w") as zf:
|
|
25
|
+
for name, content in files.items():
|
|
26
|
+
zf.writestr(name, content)
|
|
27
|
+
return buf.getvalue()
|
|
85
28
|
|
|
86
|
-
def test_filter_node_for_export_always_removes_columns_for_cubes():
|
|
87
|
-
"""Cube columns should always be removed - they're inferred from metrics/dimensions."""
|
|
88
|
-
node = {
|
|
89
|
-
"name": "test.cube",
|
|
90
|
-
"node_type": "cube",
|
|
91
|
-
"metrics": ["test.metric1", "test.metric2"],
|
|
92
|
-
"dimensions": ["test.dim1"],
|
|
93
|
-
"columns": [
|
|
94
|
-
{"name": "metric1", "type": "BIGINT"},
|
|
95
|
-
{"name": "dim1", "type": "VARCHAR", "display_name": "Dimension 1"},
|
|
96
|
-
],
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
filtered = DeploymentService.filter_node_for_export(node)
|
|
100
|
-
|
|
101
|
-
# Columns should be removed regardless of customizations
|
|
102
|
-
assert "columns" not in filtered
|
|
103
|
-
# Other fields should remain
|
|
104
|
-
assert filtered["metrics"] == ["test.metric1", "test.metric2"]
|
|
105
|
-
assert filtered["dimensions"] == ["test.dim1"]
|
|
106
29
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
30
|
+
def test_pull_extracts_zip_from_server(tmp_path):
|
|
31
|
+
zip_bytes = _make_zip(
|
|
32
|
+
{
|
|
33
|
+
"dj.yaml": "namespace: foo.bar\n",
|
|
34
|
+
"foo/bar/baz.yaml": "name: foo.bar.baz\nquery: SELECT 1\n",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
110
37
|
client = MagicMock()
|
|
111
|
-
client.
|
|
112
|
-
"namespace": "foo.bar",
|
|
113
|
-
"nodes": [
|
|
114
|
-
{"name": "foo.bar.baz", "query": "SELECT 1"},
|
|
115
|
-
{"name": "foo.bar.qux", "query": "SELECT 2"},
|
|
116
|
-
],
|
|
117
|
-
}
|
|
38
|
+
client._export_namespace_yaml_zip.return_value = zip_bytes
|
|
118
39
|
svc = DeploymentService(client)
|
|
119
40
|
|
|
120
41
|
svc.pull("foo.bar", tmp_path)
|
|
121
42
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
assert
|
|
125
|
-
|
|
126
|
-
# node files
|
|
127
|
-
baz_file = tmp_path / "foo" / "bar" / "baz.yaml"
|
|
128
|
-
assert baz_file.exists()
|
|
129
|
-
assert yaml.safe_load(baz_file.read_text())["query"] == "SELECT 1"
|
|
43
|
+
client._export_namespace_yaml_zip.assert_called_once_with("foo.bar", None)
|
|
44
|
+
assert yaml.safe_load((tmp_path / "dj.yaml").read_text())["namespace"] == "foo.bar"
|
|
45
|
+
assert (tmp_path / "foo" / "bar" / "baz.yaml").exists()
|
|
130
46
|
|
|
131
|
-
qux_file = tmp_path / "foo" / "bar" / "qux.yaml"
|
|
132
|
-
assert qux_file.exists()
|
|
133
47
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
48
|
+
def test_pull_uploads_existing_yaml_files(tmp_path):
|
|
49
|
+
(tmp_path / "old.yaml").write_text("name: ns.old\n")
|
|
50
|
+
updated_zip = _make_zip({"old.yaml": "name: ns.old\nquery: SELECT 2\n"})
|
|
137
51
|
client = MagicMock()
|
|
52
|
+
client._export_namespace_yaml_zip.return_value = updated_zip
|
|
138
53
|
svc = DeploymentService(client)
|
|
139
|
-
|
|
140
|
-
|
|
54
|
+
|
|
55
|
+
svc.pull("ns", tmp_path)
|
|
56
|
+
|
|
57
|
+
_, call_kwargs = client._export_namespace_yaml_zip.call_args
|
|
58
|
+
existing_bytes = client._export_namespace_yaml_zip.call_args[0][1]
|
|
59
|
+
assert existing_bytes is not None
|
|
60
|
+
with zipfile.ZipFile(io.BytesIO(existing_bytes)) as zf:
|
|
61
|
+
assert "old.yaml" in zf.namelist()
|
|
141
62
|
|
|
142
63
|
|
|
143
64
|
def test_print_results_success():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/dimension_link.dimension.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/is_active.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/materialization.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/node_type.dimension.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/datajunction/seed/nodes/number_of_nodes.metric.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/deploy0/roads/companies_dim.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/contractors.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/date.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/date_dim.dimension.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/dispatchers.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/hard_hat.dimension.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/hard_hats.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/municipality.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/repair_type.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_region.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_state.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project1/roads/us_states.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/avg_repair_price.metric.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/contractor.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/contractors.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/date_dim.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/dispatcher.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/dispatchers.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hat.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hat_state.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/hard_hats.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/municipality_type.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/num_repair_orders.metric.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_order.dimension.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_orders.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_orders_cube.cube.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/repair_type.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/total_repair_cost.metric.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_region.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_state.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project11/us_states.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/companies.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/contractors.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/us_state.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project12/roads/us_states.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project5/some_node.a.b.c.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project6/roads/contractors.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project7/roads/contractors.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/companies.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/contractors.source.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/us_state.dimension.yaml
RENAMED
|
File without changes
|
{datajunction-0.0.148 → datajunction-0.0.150}/tests/examples/project9/roads/us_states.source.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|