recce-nightly 1.10.0.20250629__py3-none-any.whl → 1.25.0.20251112a20664__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 (168) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +116 -74
  4. recce/artifact.py +76 -3
  5. recce/cli.py +665 -69
  6. recce/config.py +2 -2
  7. recce/connect_to_cloud.py +1 -1
  8. recce/core.py +3 -3
  9. recce/data/404.html +1 -22
  10. recce/data/__next.__PAGE__.txt +10 -0
  11. recce/data/__next._full.txt +23 -0
  12. recce/data/__next._index.txt +8 -0
  13. recce/data/__next._tree.txt +12 -0
  14. recce/data/_next/static/JwV_pqetN5WamZZ7aGdfH/_buildManifest.js +11 -0
  15. recce/data/_next/static/JwV_pqetN5WamZZ7aGdfH/_clientMiddlewareManifest.json +1 -0
  16. recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
  17. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  18. recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
  19. recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
  20. recce/data/_next/static/chunks/67b1c6a62f19d429.js +110 -0
  21. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  22. recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
  23. recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
  24. recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
  25. recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
  26. recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
  27. recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
  28. recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
  29. recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
  30. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  31. recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
  32. recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
  33. recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
  34. recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.js +3 -0
  35. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  36. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  37. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  38. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  39. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  40. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  41. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  42. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  43. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  44. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  45. recce/data/_not-found/__next._full.txt +17 -0
  46. recce/data/_not-found/__next._index.txt +8 -0
  47. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  48. recce/data/_not-found/__next._not-found.txt +4 -0
  49. recce/data/_not-found/__next._tree.txt +10 -0
  50. recce/data/_not-found.html +1 -0
  51. recce/data/_not-found.txt +17 -0
  52. recce/data/auth_callback.html +1 -1
  53. recce/data/index.html +1 -27
  54. recce/data/index.txt +23 -8
  55. recce/event/__init__.py +9 -8
  56. recce/event/collector.py +6 -2
  57. recce/event/track.py +10 -0
  58. recce/github.py +1 -1
  59. recce/mcp_server.py +632 -0
  60. recce/models/types.py +23 -2
  61. recce/pull_request.py +1 -1
  62. recce/run.py +23 -16
  63. recce/server.py +165 -11
  64. recce/state/__init__.py +31 -0
  65. recce/state/cloud.py +632 -0
  66. recce/state/const.py +26 -0
  67. recce/state/local.py +56 -0
  68. recce/state/state.py +119 -0
  69. recce/state/state_loader.py +174 -0
  70. recce/summary.py +2 -1
  71. recce/tasks/dataframe.py +59 -2
  72. recce/tasks/rowcount.py +4 -1
  73. recce/tasks/schema.py +4 -1
  74. recce/tasks/valuediff.py +1 -1
  75. recce/util/api_token.py +11 -2
  76. recce/util/breaking.py +9 -0
  77. recce/util/cll.py +1 -2
  78. recce/util/io.py +2 -2
  79. recce/util/lineage.py +14 -18
  80. recce/util/perf_tracking.py +85 -0
  81. recce/util/recce_cloud.py +229 -5
  82. recce/yaml/__init__.py +2 -2
  83. recce_cloud/__init__.py +15 -0
  84. recce_cloud/api/__init__.py +17 -0
  85. recce_cloud/api/base.py +104 -0
  86. recce_cloud/api/client.py +150 -0
  87. recce_cloud/api/exceptions.py +26 -0
  88. recce_cloud/api/factory.py +63 -0
  89. recce_cloud/api/github.py +72 -0
  90. recce_cloud/api/gitlab.py +78 -0
  91. recce_cloud/artifact.py +57 -0
  92. recce_cloud/ci_providers/__init__.py +9 -0
  93. recce_cloud/ci_providers/base.py +82 -0
  94. recce_cloud/ci_providers/detector.py +147 -0
  95. recce_cloud/ci_providers/github_actions.py +136 -0
  96. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  97. recce_cloud/cli.py +303 -0
  98. recce_cloud/upload.py +213 -0
  99. {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/METADATA +31 -27
  100. recce_nightly-1.25.0.20251112a20664.dist-info/RECORD +178 -0
  101. {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/top_level.txt +1 -0
  102. tests/adapter/dbt_adapter/test_dbt_cll.py +68 -17
  103. tests/recce_cloud/__init__.py +0 -0
  104. tests/recce_cloud/test_ci_providers.py +351 -0
  105. tests/recce_cloud/test_cli.py +372 -0
  106. tests/recce_cloud/test_client.py +273 -0
  107. tests/recce_cloud/test_platform_clients.py +279 -0
  108. tests/test_cli.py +106 -3
  109. tests/test_cli_mcp_optional.py +45 -0
  110. tests/test_cloud_listing_cli.py +324 -0
  111. tests/test_core.py +147 -0
  112. tests/test_mcp_server.py +332 -0
  113. tests/test_server.py +6 -6
  114. tests/test_summary.py +14 -6
  115. recce/data/_next/static/Mrb9CZ3toH6Q8xrzNzCrg/_buildManifest.js +0 -1
  116. recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
  117. recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
  118. recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
  119. recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
  120. recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
  121. recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
  122. recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
  123. recce/data/_next/static/chunks/41-f30276c289169376.js +0 -9
  124. recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
  125. recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
  126. recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
  127. recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
  128. recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
  129. recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
  130. recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
  131. recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
  132. recce/data/_next/static/chunks/92-68460b15fe448f33.js +0 -1
  133. recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
  134. recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
  135. recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
  136. recce/data/_next/static/chunks/app/layout-292f035bb0d2a98e.js +0 -1
  137. recce/data/_next/static/chunks/app/page-598f8acc82179d01.js +0 -1
  138. recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
  139. recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
  140. recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
  141. recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
  142. recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
  143. recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
  144. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  145. recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
  146. recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
  147. recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
  148. recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
  149. recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
  150. recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
  151. recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
  152. recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
  153. recce/data/_next/static/css/a2b12b4ba4227f0a.css +0 -3
  154. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  155. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  156. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  157. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  158. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  159. recce/state.py +0 -786
  160. recce_nightly-1.10.0.20250629.dist-info/RECORD +0 -154
  161. tests/test_state.py +0 -134
  162. /recce/data/_next/static/{Mrb9CZ3toH6Q8xrzNzCrg → JwV_pqetN5WamZZ7aGdfH}/_ssgManifest.js +0 -0
  163. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  164. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  165. /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
  166. {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/WHEEL +0 -0
  167. {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/entry_points.txt +0 -0
  168. {recce_nightly-1.10.0.20250629.dist-info → recce_nightly-1.25.0.20251112a20664.dist-info}/licenses/LICENSE +0 -0
recce/VERSION CHANGED
@@ -1 +1 @@
1
- 1.10.0.20250629
1
+ 1.25.0.20251112a20664
recce/__init__.py CHANGED
@@ -35,6 +35,11 @@ def is_ci_env():
35
35
  return False
36
36
 
37
37
 
38
+ def is_recce_cloud_instance():
39
+ """Check if running in Recce Cloud instance."""
40
+ return os.environ.get("RECCE_CLOUD_INSTANCE", "false").lower() == "true"
41
+
42
+
38
43
  def get_runner():
39
44
  # GitHub Action
40
45
  if os.environ.get("GITHUB_ACTIONS", "false") == "true":
@@ -31,9 +31,10 @@ from recce.util.lineage import (
31
31
  find_downstream,
32
32
  find_upstream,
33
33
  )
34
+ from recce.util.perf_tracking import LineagePerfTracker
34
35
 
35
36
  from ...tasks.profile import ProfileTask
36
- from ...util.breaking import parse_change_category
37
+ from ...util.breaking import BreakingPerformanceTracking, parse_change_category
37
38
 
38
39
  try:
39
40
  import agate
@@ -278,7 +279,7 @@ class DbtArgs:
278
279
  target_path: Optional[str] = (None,)
279
280
  project_only_flags: Optional[Dict[str, Any]] = None
280
281
  which: Optional[str] = None
281
- state_modified_compare_more_unrendered_values: Optional[bool] = False # new flag added since dbt v1.9
282
+ state_modified_compare_more_unrendered_values: Optional[bool] = True # new flag added since dbt v1.9
282
283
 
283
284
 
284
285
  @dataclass
@@ -407,7 +408,7 @@ class DbtAdapter(BaseAdapter):
407
408
 
408
409
  if self.adapter.connections.TYPE == "databricks":
409
410
  # reference: get_columns_in_relation (dbt/adapters/databricks/impl.py)
410
- from dbt.adapters.databricks import DatabricksColumn
411
+ from dbt.adapters.databricks.column import DatabricksColumn
411
412
 
412
413
  rows = columns
413
414
  columns = []
@@ -599,7 +600,15 @@ class DbtAdapter(BaseAdapter):
599
600
  return node.compiled_code
600
601
  else:
601
602
  from dbt.clients import jinja
602
- from dbt.context.providers import generate_runtime_model_context
603
+ from dbt.context.providers import (
604
+ generate_runtime_macro_context,
605
+ generate_runtime_model_context,
606
+ )
607
+
608
+ # Set up macro resolver for dbt >= 1.8
609
+ macro_manifest = MacroManifest(manifest.macros)
610
+ self.adapter.set_macro_resolver(macro_manifest)
611
+ self.adapter.set_macro_context_generator(generate_runtime_macro_context)
603
612
 
604
613
  jinja_ctx = generate_runtime_model_context(node, self.runtime_config, manifest)
605
614
  jinja_ctx.update(context)
@@ -658,8 +667,8 @@ class DbtAdapter(BaseAdapter):
658
667
  @lru_cache(maxsize=2)
659
668
  def get_lineage_cached(self, base: Optional[bool] = False, cache_key=0):
660
669
  if base is False:
661
- cll_tracker = CLLPerformanceTracking()
662
- cll_tracker.start_lineage()
670
+ perf_tracker = LineagePerfTracker()
671
+ perf_tracker.start_lineage()
663
672
 
664
673
  manifest = self.curr_manifest if base is False else self.base_manifest
665
674
  catalog = self.curr_catalog if base is False else self.base_catalog
@@ -736,6 +745,7 @@ class DbtAdapter(BaseAdapter):
736
745
  nodes[unique_id] = {
737
746
  "id": source["unique_id"],
738
747
  "name": source["name"],
748
+ "source_name": source["source_name"],
739
749
  "resource_type": source["resource_type"],
740
750
  "package_name": source["package_name"],
741
751
  "config": source["config"],
@@ -777,10 +787,10 @@ class DbtAdapter(BaseAdapter):
777
787
  parent_map = self.build_parent_map(nodes, base)
778
788
 
779
789
  if base is False:
780
- cll_tracker.end_lineage()
781
- cll_tracker.set_total_nodes(len(nodes))
782
- log_performance("model lineage", cll_tracker.to_dict())
783
- cll_tracker.reset()
790
+ perf_tracker.end_lineage()
791
+ perf_tracker.set_total_nodes(len(nodes))
792
+ log_performance("model lineage", perf_tracker.to_dict())
793
+ perf_tracker.reset()
784
794
 
785
795
  return dict(
786
796
  parent_map=parent_map,
@@ -814,17 +824,22 @@ class DbtAdapter(BaseAdapter):
814
824
 
815
825
  @lru_cache(maxsize=128)
816
826
  def get_change_analysis_cached(self, node_id: str):
827
+ breaking_perf_tracker = BreakingPerformanceTracking()
817
828
  lineage_diff = self.get_lineage_diff()
818
829
  diff = lineage_diff.diff
819
830
 
820
831
  if node_id not in diff or diff[node_id].change_status != "modified":
821
832
  return diff.get(node_id)
822
833
 
834
+ breaking_perf_tracker.increment_modified_nodes()
835
+ breaking_perf_tracker.start_lineage_diff()
836
+
823
837
  base = lineage_diff.base
824
838
  current = lineage_diff.current
825
839
 
826
840
  base_manifest = as_manifest(self.get_manifest(True))
827
841
  curr_manifest = as_manifest(self.get_manifest(False))
842
+ breaking_perf_tracker.record_checkpoint("manifest")
828
843
 
829
844
  def ref_func(*args):
830
845
  if len(args) == 1:
@@ -895,6 +910,7 @@ class DbtAdapter(BaseAdapter):
895
910
  old_schema=base_schema,
896
911
  new_schema=curr_schema,
897
912
  dialect=dialect,
913
+ perf_tracking=breaking_perf_tracker,
898
914
  )
899
915
 
900
916
  # Make sure that the case of the column names are the same
@@ -917,6 +933,9 @@ class DbtAdapter(BaseAdapter):
917
933
  # TODO: telemetry
918
934
  pass
919
935
 
936
+ breaking_perf_tracker.end_lineage_diff()
937
+ log_performance("change analysis per node", breaking_perf_tracker.to_dict())
938
+ breaking_perf_tracker.reset()
920
939
  node_diff = diff.get(node_id)
921
940
  node_diff.change = change
922
941
  return node_diff
@@ -931,7 +950,15 @@ class DbtAdapter(BaseAdapter):
931
950
  no_downstream: Optional[bool] = False,
932
951
  no_filter: Optional[bool] = False,
933
952
  ) -> CllData:
934
- cll_tracker = CLLPerformanceTracking()
953
+ cll_tracker = LineagePerfTracker()
954
+ cll_tracker.set_params(
955
+ has_node=node_id is not None,
956
+ has_column=column is not None,
957
+ change_analysis=change_analysis,
958
+ no_cll=no_cll,
959
+ no_upstream=no_upstream,
960
+ no_downstream=no_downstream,
961
+ )
935
962
  cll_tracker.start_column_lineage()
936
963
 
937
964
  manifest = self.curr_manifest
@@ -944,6 +971,8 @@ class DbtAdapter(BaseAdapter):
944
971
  lineage_diff = self.get_lineage_diff()
945
972
  cll_node_ids = set(lineage_diff.diff.keys())
946
973
 
974
+ cll_tracker.set_init_nodes(len(cll_node_ids))
975
+
947
976
  nodes = {}
948
977
  columns = {}
949
978
  parent_map = {}
@@ -955,29 +984,41 @@ class DbtAdapter(BaseAdapter):
955
984
  cll_node_ids = cll_node_ids.union(find_downstream(cll_node_ids, manifest_dict.get("child_map")))
956
985
 
957
986
  if not no_cll:
987
+ allowed_related_nodes = set()
988
+ for key in ["sources", "nodes", "exposures", "metrics"]:
989
+ attr = getattr(manifest, key)
990
+ allowed_related_nodes.update(set(attr.keys()))
991
+ if hasattr(manifest, "semantic_models"):
992
+ attr = getattr(manifest, "semantic_models")
993
+ allowed_related_nodes.update(set(attr.keys()))
958
994
  for cll_node_id in cll_node_ids:
959
- if (
960
- cll_node_id not in manifest.sources
961
- and cll_node_id not in manifest.nodes
962
- and cll_node_id not in manifest.exposures
963
- ):
995
+ if cll_node_id not in allowed_related_nodes:
964
996
  continue
965
997
  cll_data_one = deepcopy(self.get_cll_cached(cll_node_id, base=False))
998
+ cll_tracker.increment_cll_nodes()
966
999
  if cll_data_one is None:
967
1000
  continue
968
1001
 
969
1002
  nodes[cll_node_id] = cll_data_one.nodes.get(cll_node_id)
970
- node_diff = self.get_change_analysis_cached(cll_node_id) if change_analysis else None
1003
+ node_diff = None
1004
+ if change_analysis:
1005
+ node_diff = self.get_change_analysis_cached(cll_node_id)
1006
+ cll_tracker.increment_change_analysis_nodes()
971
1007
  if node_diff is not None:
972
1008
  nodes[cll_node_id].change_status = node_diff.change_status
973
1009
  if node_diff.change is not None:
974
1010
  nodes[cll_node_id].change_category = node_diff.change.category
975
1011
  for c_id, c in cll_data_one.columns.items():
976
1012
  columns[c_id] = c
977
- if node_diff is not None and node_diff.change is not None and node_diff.change.columns is not None:
978
- column_diff = node_diff.change.columns.get(c.name)
979
- if column_diff:
980
- c.change_status = column_diff
1013
+ if node_diff is not None:
1014
+ if node_diff.change_status == "added":
1015
+ c.change_status = "added"
1016
+ elif node_diff.change_status == "removed":
1017
+ c.change_status = "removed"
1018
+ elif node_diff.change is not None and node_diff.change.columns is not None:
1019
+ column_diff = node_diff.change.columns.get(c.name)
1020
+ if column_diff:
1021
+ c.change_status = column_diff
981
1022
 
982
1023
  for p_id, parents in cll_data_one.parent_map.items():
983
1024
  parent_map[p_id] = parents
@@ -987,13 +1028,7 @@ class DbtAdapter(BaseAdapter):
987
1028
  cll_node_columns: Dict[str, CllColumn] = {}
988
1029
 
989
1030
  if cll_node_id in manifest.sources:
990
- n = manifest.sources[cll_node_id]
991
- cll_node = CllNode(
992
- id=n.unique_id,
993
- name=n.name,
994
- source_name=n.source_name,
995
- package_name=n.package_name,
996
- )
1031
+ cll_node = CllNode.build_cll_node(manifest, "sources", cll_node_id)
997
1032
  if self.curr_catalog and cll_node_id in self.curr_catalog.sources:
998
1033
  cll_node_columns = {
999
1034
  column.name: CllColumn(
@@ -1005,15 +1040,7 @@ class DbtAdapter(BaseAdapter):
1005
1040
  for column in self.curr_catalog.sources[cll_node_id].columns.values()
1006
1041
  }
1007
1042
  elif cll_node_id in manifest.nodes:
1008
- n = manifest.nodes[cll_node_id]
1009
- if n.resource_type not in ["model", "seed", "snapshot"]:
1010
- continue
1011
- cll_node = CllNode(
1012
- id=n.unique_id,
1013
- name=n.name,
1014
- package_name=n.package_name,
1015
- resource_type=n.resource_type,
1016
- )
1043
+ cll_node = CllNode.build_cll_node(manifest, "nodes", cll_node_id)
1017
1044
  if self.curr_catalog and cll_node_id in self.curr_catalog.nodes:
1018
1045
  cll_node_columns = {
1019
1046
  column.name: CllColumn(
@@ -1025,19 +1052,20 @@ class DbtAdapter(BaseAdapter):
1025
1052
  for column in self.curr_catalog.nodes[cll_node_id].columns.values()
1026
1053
  }
1027
1054
  elif cll_node_id in manifest.exposures:
1028
- n = manifest.exposures[cll_node_id]
1029
- cll_node = CllNode(
1030
- id=n.unique_id,
1031
- name=n.name,
1032
- package_name=n.package_name,
1033
- resource_type=n.resource_type,
1034
- )
1055
+ cll_node = CllNode.build_cll_node(manifest, "exposures", cll_node_id)
1056
+ elif hasattr(manifest, "semantic_models") and cll_node_id in manifest.semantic_models:
1057
+ cll_node = CllNode.build_cll_node(manifest, "semantic_models", cll_node_id)
1058
+ elif cll_node_id in manifest.metrics:
1059
+ cll_node = CllNode.build_cll_node(manifest, "metrics", cll_node_id)
1035
1060
 
1036
1061
  if not cll_node:
1037
1062
  continue
1038
1063
  nodes[cll_node_id] = cll_node
1039
1064
 
1040
- node_diff = self.get_change_analysis_cached(cll_node_id) if change_analysis else None
1065
+ node_diff = None
1066
+ if change_analysis:
1067
+ node_diff = self.get_change_analysis_cached(cll_node_id)
1068
+ cll_tracker.increment_change_analysis_nodes()
1041
1069
  if node_diff is not None:
1042
1070
  cll_node.change_status = node_diff.change_status
1043
1071
  if node_diff.change is not None:
@@ -1063,7 +1091,19 @@ class DbtAdapter(BaseAdapter):
1063
1091
  if node_id is None and column is None:
1064
1092
  if change_analysis:
1065
1093
  # If change analysis is requested, we need to find the nodes that have changes
1066
- for nid in self.get_lineage_diff().diff.keys():
1094
+ lineage_diff = self.get_lineage_diff()
1095
+ for nid, nd in lineage_diff.diff.items():
1096
+ if nd.change_status == "added":
1097
+ anchor_node_ids.add(nid)
1098
+ n = lineage_diff.current["nodes"].get(nid)
1099
+ n_columns = n.get("columns", {})
1100
+ for c in n_columns:
1101
+ anchor_node_ids.add(build_column_key(nid, c))
1102
+ continue
1103
+ if nd.change_status == "removed":
1104
+ extra_node_ids.add(nid)
1105
+ continue
1106
+
1067
1107
  node_diff = self.get_change_analysis_cached(nid)
1068
1108
  if node_diff is not None and node_diff.change is not None:
1069
1109
  extra_node_ids.add(nid)
@@ -1107,6 +1147,7 @@ class DbtAdapter(BaseAdapter):
1107
1147
  else:
1108
1148
  anchor_node_ids.add(f"{node_id}_{column}")
1109
1149
 
1150
+ cll_tracker.set_anchor_nodes(len(anchor_node_ids))
1110
1151
  result_node_ids = set(anchor_node_ids)
1111
1152
  if not no_upstream:
1112
1153
  result_node_ids = result_node_ids.union(find_upstream(anchor_node_ids, parent_map))
@@ -1122,10 +1163,14 @@ class DbtAdapter(BaseAdapter):
1122
1163
  node.columns = {
1123
1164
  k: v for k, v in node.columns.items() if v.id in result_node_ids or v.id in extra_node_ids
1124
1165
  }
1166
+
1167
+ if change_analysis:
1168
+ node.impacted = node.id in result_node_ids
1169
+
1125
1170
  parent_map, child_map = filter_dependency_maps(parent_map, child_map, result_node_ids)
1126
1171
 
1127
1172
  cll_tracker.end_column_lineage()
1128
- cll_tracker.set_total_nodes(len(nodes))
1173
+ cll_tracker.set_total_nodes(len(nodes) + len(columns))
1129
1174
  log_performance("column level lineage", cll_tracker.to_dict())
1130
1175
  cll_tracker.reset()
1131
1176
 
@@ -1144,6 +1189,9 @@ class DbtAdapter(BaseAdapter):
1144
1189
  if node is None:
1145
1190
  return None
1146
1191
 
1192
+ cll_tracker.set_total_nodes(1)
1193
+ cll_tracker.start_column_lineage()
1194
+
1147
1195
  def _apply_all_columns(node: CllNode, transformation_type):
1148
1196
  cll_data = CllData()
1149
1197
  cll_data.nodes[node.id] = node
@@ -1268,6 +1316,10 @@ class DbtAdapter(BaseAdapter):
1268
1316
  depends_on.add(parent_key)
1269
1317
  column.transformation_type = c2c_map[name].transformation_type
1270
1318
  cll_data.parent_map[column_id] = set(depends_on)
1319
+
1320
+ cll_tracker.end_column_lineage()
1321
+ log_performance("column level lineage per node", cll_tracker.to_dict())
1322
+ cll_tracker.reset()
1271
1323
  return cll_data
1272
1324
 
1273
1325
  def get_cll_node(self, node_id: str, base: Optional[bool] = False) -> Tuple[Optional[CllNode], list[str]]:
@@ -1279,21 +1331,12 @@ class DbtAdapter(BaseAdapter):
1279
1331
  # model, seed, snapshot
1280
1332
  if node_id in manifest.nodes:
1281
1333
  found = manifest.nodes[node_id]
1282
- if found.resource_type not in ["model", "seed", "snapshot"]:
1283
- return None, []
1284
-
1285
1334
  unique_id = found.unique_id
1286
- node = CllNode(
1287
- id=found.unique_id,
1288
- name=found.name,
1289
- package_name=found.package_name,
1290
- resource_type=found.resource_type,
1291
- raw_code=found.raw_code,
1292
- )
1335
+ node = CllNode.build_cll_node(manifest, "nodes", node_id)
1293
1336
  if hasattr(found.depends_on, "nodes"):
1294
1337
  parent_list = found.depends_on.nodes
1295
1338
 
1296
- if catalog is not None and unique_id in catalog.nodes:
1339
+ if catalog is not None and node is not None and unique_id in catalog.nodes:
1297
1340
  columns = {}
1298
1341
  for col_name, col_metadata in catalog.nodes[unique_id].columns.items():
1299
1342
  column_id = f"{unique_id}_{col_name}"
@@ -1305,17 +1348,10 @@ class DbtAdapter(BaseAdapter):
1305
1348
  if node_id in manifest.sources:
1306
1349
  found = manifest.sources[node_id]
1307
1350
  unique_id = found.unique_id
1308
-
1309
- node = CllNode(
1310
- id=found.unique_id,
1311
- name=found.name,
1312
- package_name=found.package_name,
1313
- resource_type=found.resource_type,
1314
- source_name=found.source_name,
1315
- )
1351
+ node = CllNode.build_cll_node(manifest, "sources", node_id)
1316
1352
  parent_list = []
1317
1353
 
1318
- if catalog is not None and unique_id in catalog.sources:
1354
+ if catalog is not None and node is not None and unique_id in catalog.sources:
1319
1355
  columns = {}
1320
1356
  for col_name, col_metadata in catalog.sources[unique_id].columns.items():
1321
1357
  column_id = f"{unique_id}_{col_name}"
@@ -1326,13 +1362,19 @@ class DbtAdapter(BaseAdapter):
1326
1362
  # exposure
1327
1363
  if node_id in manifest.exposures:
1328
1364
  found = manifest.exposures[node_id]
1365
+ node = CllNode.build_cll_node(manifest, "exposures", node_id)
1366
+ if hasattr(found.depends_on, "nodes"):
1367
+ parent_list = found.depends_on.nodes
1329
1368
 
1330
- node = CllNode(
1331
- id=found.unique_id,
1332
- name=found.name,
1333
- package_name=found.package_name,
1334
- resource_type=found.resource_type,
1335
- )
1369
+ if hasattr(manifest, "semantic_models") and node_id in manifest.semantic_models:
1370
+ found = manifest.semantic_models[node_id]
1371
+ node = CllNode.build_cll_node(manifest, "semantic_models", node_id)
1372
+ if hasattr(found.depends_on, "nodes"):
1373
+ parent_list = found.depends_on.nodes
1374
+
1375
+ if node_id in manifest.metrics:
1376
+ found = manifest.metrics[node_id]
1377
+ node = CllNode.build_cll_node(manifest, "metrics", node_id)
1336
1378
  if hasattr(found.depends_on, "nodes"):
1337
1379
  parent_list = found.depends_on.nodes
1338
1380
 
@@ -1558,7 +1600,7 @@ class DbtAdapter(BaseAdapter):
1558
1600
  if not os.path.isfile(path):
1559
1601
  return None
1560
1602
 
1561
- with open(path, "r") as f:
1603
+ with open(path, "r", encoding="utf-8") as f:
1562
1604
  json_content = f.read()
1563
1605
  return json.loads(json_content)
1564
1606
 
recce/artifact.py CHANGED
@@ -40,7 +40,7 @@ def verify_artifacts_path(target_path: str) -> bool:
40
40
 
41
41
 
42
42
  def parse_dbt_version(file_path: str) -> str:
43
- with open(file_path, "r") as f:
43
+ with open(file_path, "r", encoding="utf-8") as f:
44
44
  data = json.load(f)
45
45
 
46
46
  dbt_version = data.get("metadata", {}).get("dbt_version", None)
@@ -80,6 +80,64 @@ def archive_artifacts(target_path: str) -> (str, str):
80
80
  return artifacts_tar_gz_path, dbt_version
81
81
 
82
82
 
83
+ def upload_artifacts_to_session(target_path: str, session_id: str, token: str, debug: bool = False):
84
+ """Upload dbt artifacts to a specific session ID in Recce Cloud."""
85
+ console = Console()
86
+ if verify_artifacts_path(target_path) is False:
87
+ console.print(f"[[red]Error[/red]] Invalid target path: {target_path}")
88
+ console.print("Please provide a valid target path containing manifest.json and catalog.json.")
89
+ return 1
90
+
91
+ manifest_path = os.path.join(target_path, "manifest.json")
92
+ catalog_path = os.path.join(target_path, "catalog.json")
93
+
94
+ # get the adapter type from the manifest file
95
+ with open(manifest_path, "r", encoding="utf-8") as f:
96
+ manifest_data = json.load(f)
97
+ adapter_type = manifest_data.get("metadata", {}).get("adapter_type")
98
+ if adapter_type is None:
99
+ raise Exception("Failed to parse adapter type from manifest.json")
100
+
101
+ recce_cloud = RecceCloud(token)
102
+
103
+ session = recce_cloud.get_session(session_id)
104
+
105
+ org_id = session.get("org_id")
106
+ if org_id is None:
107
+ raise Exception(f"Session ID {session_id} does not belong to any organization.")
108
+
109
+ project_id = session.get("project_id")
110
+ if project_id is None:
111
+ raise Exception(f"Session ID {session_id} does not belong to any project.")
112
+
113
+ # Get the presigned URL for uploading the artifacts using session ID
114
+ console.print(f'Uploading artifacts for session ID "{session_id}"')
115
+ presigned_urls = recce_cloud.get_upload_urls_by_session_id(org_id, project_id, session_id)
116
+ if debug:
117
+ console.rule("Debug information", style="blue")
118
+ console.print(f"Org ID: {org_id}")
119
+ console.print(f"Project ID: {project_id}")
120
+ console.print(f"Session ID: {session_id}")
121
+ console.print(f"Manifest path: {presigned_urls['manifest_url']}")
122
+ console.print(f"Catalog path: {presigned_urls['catalog_url']}")
123
+ console.print(f"Adapter type: {adapter_type}")
124
+
125
+ # Upload the compressed artifacts (no password needed for session uploads)
126
+ console.print(f'Uploading manifest from path "{manifest_path}"')
127
+ response = requests.put(presigned_urls["manifest_url"], data=open(manifest_path, "rb").read())
128
+ if response.status_code != 200 and response.status_code != 204:
129
+ raise Exception(response.text)
130
+ console.print(f'Uploading catalog from path "{catalog_path}"')
131
+ response = requests.put(presigned_urls["catalog_url"], data=open(catalog_path, "rb").read())
132
+ if response.status_code != 200 and response.status_code != 204:
133
+ raise Exception(response.text)
134
+
135
+ # Update the session metadata
136
+ recce_cloud.update_session(org_id, project_id, session_id, adapter_type)
137
+
138
+ return 0
139
+
140
+
83
141
  def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: str, debug: bool = False):
84
142
  console = Console()
85
143
  if verify_artifacts_path(target_path) is False:
@@ -100,7 +158,7 @@ def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: st
100
158
  metadata = {"commit": sha, "dbt_version": dbt_version}
101
159
 
102
160
  # Get the presigned URL for uploading the artifacts
103
- presigned_url = RecceCloud(token).get_presigned_url(
161
+ presigned_url = RecceCloud(token).get_presigned_url_by_github_repo(
104
162
  method=PresignedUrlMethod.UPLOAD,
105
163
  repository=repo,
106
164
  artifact_name="dbt_artifacts.tar.gz",
@@ -145,7 +203,7 @@ def download_dbt_artifacts(
145
203
  sha = None
146
204
  dbt_version = None
147
205
 
148
- presigned_url, tags = RecceCloud(token).get_download_presigned_url_with_tags(
206
+ presigned_url, tags = RecceCloud(token).get_download_presigned_url_by_github_repo_with_tags(
149
207
  repository=repo,
150
208
  artifact_name="dbt_artifacts.tar.gz",
151
209
  branch=branch,
@@ -191,3 +249,18 @@ def download_dbt_artifacts(
191
249
  except FileNotFoundError:
192
250
  pass
193
251
  return 0
252
+
253
+
254
+ def delete_dbt_artifacts(branch: str, token: str, debug: bool = False):
255
+ """Delete dbt artifacts from a specific branch in Recce Cloud."""
256
+ console = Console()
257
+ repo = hosting_repo()
258
+
259
+ if debug:
260
+ console.rule("Debug information", style="blue")
261
+ console.print(f"Git Branch: {branch}")
262
+ console.print(f"GitHub repository: {repo}")
263
+
264
+ console.print(f'Deleting dbt artifacts from branch: "{branch}"')
265
+
266
+ RecceCloud(token).purge_artifacts(repo, branch=branch)