recce-nightly 1.9.0.20250622__py3-none-any.whl → 1.9.0.20250624__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.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

Files changed (28) hide show
  1. recce/VERSION +1 -1
  2. recce/adapter/dbt_adapter/__init__.py +108 -45
  3. recce/cli.py +42 -11
  4. recce/connect_to_cloud.py +138 -0
  5. recce/data/404.html +2 -2
  6. recce/data/_next/static/chunks/{92-7ab55ae02606193c.js → 92-607cd1af83c41f43.js} +1 -1
  7. recce/data/_next/static/chunks/app/page-3f79c2e6d6f03a09.js +1 -0
  8. recce/data/auth_callback.html +68 -0
  9. recce/data/index.html +2 -2
  10. recce/data/index.txt +3 -3
  11. recce/models/types.py +7 -0
  12. recce/server.py +37 -33
  13. recce/state.py +3 -2
  14. recce/util/api_token.py +1 -9
  15. recce/util/lineage.py +16 -12
  16. recce/util/onboarding_state.py +45 -0
  17. recce/util/recce_cloud.py +7 -7
  18. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/METADATA +1 -1
  19. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/RECORD +27 -23
  20. tests/adapter/dbt_adapter/test_dbt_cll.py +9 -9
  21. tests/test_connect_to_cloud.py +82 -0
  22. recce/data/_next/static/chunks/app/page-707b8e8d98d7f780.js +0 -1
  23. /recce/data/_next/static/{M3_t2h-wA4_IXOtKHdJqV → Iga3yMdHBVJ_Vhluh2SdE}/_buildManifest.js +0 -0
  24. /recce/data/_next/static/{M3_t2h-wA4_IXOtKHdJqV → Iga3yMdHBVJ_Vhluh2SdE}/_ssgManifest.js +0 -0
  25. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/WHEEL +0 -0
  26. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/entry_points.txt +0 -0
  27. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/licenses/LICENSE +0 -0
  28. {recce_nightly-1.9.0.20250622.dist-info → recce_nightly-1.9.0.20250624.dist-info}/top_level.txt +0 -0
recce/server.py CHANGED
@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
8
8
  from dataclasses import dataclass
9
9
  from datetime import datetime, timedelta
10
10
  from pathlib import Path
11
- from typing import Annotated, Any, Dict, Literal, Optional, Set
11
+ from typing import Annotated, Any, Literal, Optional, Set
12
12
 
13
13
  from fastapi import (
14
14
  BackgroundTasks,
@@ -33,8 +33,15 @@ from . import __latest_version__, __version__, event
33
33
  from .apis.check_api import check_router
34
34
  from .apis.run_api import run_router
35
35
  from .config import RecceConfig
36
+ from .connect_to_cloud import (
37
+ connect_to_cloud_background_task,
38
+ generate_key_pair,
39
+ get_connection_url,
40
+ is_callback_server_running,
41
+ prepare_connection_url,
42
+ )
36
43
  from .core import RecceContext, default_context, load_context
37
- from .event import log_api_event, log_single_env_event
44
+ from .event import get_recce_api_token, log_api_event, log_single_env_event
38
45
  from .exceptions import RecceException
39
46
  from .models.types import CllData
40
47
  from .run import load_preset_checks
@@ -247,8 +254,7 @@ async def recce_instance_info():
247
254
  read_only = flag.get("read_only", False)
248
255
  single_env = flag.get("single_env_onboarding", False)
249
256
 
250
- auth_options = app_state.auth_options or {}
251
- api_token = auth_options.get("api_token")
257
+ api_token = get_recce_api_token()
252
258
 
253
259
  return {
254
260
  "read_only": read_only,
@@ -271,13 +277,6 @@ async def config_flag():
271
277
  return flag
272
278
 
273
279
 
274
- @app.post("/api/onboarding/completed", status_code=204)
275
- async def mark_onboarding_completed():
276
- context = default_context()
277
- context.mark_onboarding_completed()
278
- app.state.flag["show_onboarding_guide"] = False
279
-
280
-
281
280
  @app.post("/api/relaunch-hint/completed", status_code=204)
282
281
  async def mark_relaunch_hint_completed():
283
282
  app.state.flag["show_relaunch_hint"] = False
@@ -335,7 +334,12 @@ async def get_info():
335
334
 
336
335
 
337
336
  class CllIn(BaseModel):
338
- params: Dict
337
+ node_id: Optional[str] = None
338
+ column: Optional[str] = None
339
+ change_analysis: Optional[bool] = False
340
+ cll: Optional[bool] = False
341
+ upstream: Optional[bool] = False
342
+ downstream: Optional[bool] = False
339
343
 
340
344
 
341
345
  class CllOutput(BaseModel):
@@ -347,27 +351,13 @@ async def column_level_lineage_by_node(cll_input: CllIn):
347
351
  from recce.adapter.dbt_adapter import DbtAdapter
348
352
 
349
353
  dbt_adapter: DbtAdapter = default_context().adapter
350
- node_id = cll_input.params.get("node_id")
351
- column = cll_input.params.get("column")
352
-
353
- cll = dbt_adapter.get_cll(node_id, column)
354
-
355
- return CllOutput(current=cll)
356
-
357
-
358
- class ImpactRadiusIn(BaseModel):
359
- node_id: str
360
-
361
-
362
- @app.post("/api/impact-radius", response_model=CllOutput)
363
- async def impact_radius_by_node(impact_input: ImpactRadiusIn):
364
- from recce.adapter.dbt_adapter import DbtAdapter
365
-
366
- context = default_context()
367
- dbt_adapter: DbtAdapter = context.adapter
368
- node_id = impact_input.node_id
369
-
370
- cll = dbt_adapter.get_impact_radius(node_id)
354
+ cll = dbt_adapter.get_cll(
355
+ node_id=cll_input.node_id,
356
+ column=cll_input.column,
357
+ change_analysis=cll_input.change_analysis,
358
+ upstream=cll_input.upstream,
359
+ downstream=cll_input.downstream,
360
+ )
371
361
 
372
362
  return CllOutput(current=cll)
373
363
 
@@ -670,6 +660,20 @@ async def broadcast(data: str):
670
660
  await client.send_text(data)
671
661
 
672
662
 
663
+ @app.post("/api/connect")
664
+ async def generate_connect_to_cloud_url(background_tasks: BackgroundTasks):
665
+ if is_callback_server_running():
666
+ return {"connection_url": get_connection_url()}
667
+
668
+ private_key, public_key = generate_key_pair()
669
+ connection_url, callback_port = prepare_connection_url(public_key)
670
+
671
+ background_tasks.add_task(connect_to_cloud_background_task, private_key, callback_port, connection_url)
672
+ return {
673
+ "connection_url": connection_url,
674
+ }
675
+
676
+
673
677
  api_prefix = "/api"
674
678
  app.include_router(check_router, prefix=api_prefix)
675
679
  app.include_router(run_router, prefix=api_prefix)
recce/state.py CHANGED
@@ -16,6 +16,7 @@ import botocore.exceptions
16
16
  from pydantic import BaseModel, Field
17
17
 
18
18
  from recce import get_version
19
+ from recce.event import get_recce_api_token
19
20
  from recce.git import current_branch
20
21
  from recce.models import CheckDAO
21
22
  from recce.models.types import Check, Run
@@ -766,7 +767,7 @@ class RecceShareStateManager:
766
767
  self.hint_message = None
767
768
 
768
769
  def verify(self) -> bool:
769
- if self.auth_options.get("api_token") is None:
770
+ if get_recce_api_token() is None:
770
771
  self.error_message = RECCE_API_TOKEN_MISSING.error_message
771
772
  self.hint_message = RECCE_API_TOKEN_MISSING.hint_message
772
773
  return False
@@ -781,5 +782,5 @@ class RecceShareStateManager:
781
782
 
782
783
  with tempfile.NamedTemporaryFile() as tmp:
783
784
  state.to_file(tmp.name, file_type=SupportedFileTypes.FILE)
784
- response = RecceCloud(token=self.auth_options.get("api_token")).share_state(file_name, open(tmp.name, "rb"))
785
+ response = RecceCloud(token=get_recce_api_token()).share_state(file_name, open(tmp.name, "rb"))
785
786
  return response
recce/util/api_token.py CHANGED
@@ -7,8 +7,6 @@ from recce.exceptions import RecceConfigException
7
7
  from recce.util.recce_cloud import (
8
8
  RECCE_CLOUD_BASE_URL,
9
9
  RecceCloud,
10
- get_recce_cloud_onboarding_state,
11
- set_recce_cloud_onboarding_state,
12
10
  )
13
11
 
14
12
  console = Console()
@@ -54,7 +52,7 @@ def prepare_api_token(
54
52
  event.log_connected_to_cloud()
55
53
  else:
56
54
  # No api_token provided
57
- if interaction is True:
55
+ if interaction:
58
56
  console.print(
59
57
  "An API token is required for this feature. This can be obtained in your user account settings.\n"
60
58
  f"{RECCE_CLOUD_BASE_URL}/settings#tokens\n"
@@ -70,10 +68,4 @@ def prepare_api_token(
70
68
  "You no longer need to append --api-token to the recce command"
71
69
  )
72
70
 
73
- if api_token:
74
- cloud_onboarding_state = get_recce_cloud_onboarding_state(api_token)
75
- if cloud_onboarding_state == "new":
76
- # Mark the onboarding state as "launched" if the user is new
77
- set_recce_cloud_onboarding_state(api_token, "launched")
78
-
79
71
  return api_token
recce/util/lineage.py CHANGED
@@ -1,9 +1,9 @@
1
- from typing import Dict, Set, Tuple
1
+ from typing import Dict, Iterable, Set, Tuple
2
2
 
3
3
  from recce.models.types import CllColumn, CllNode
4
4
 
5
5
 
6
- def find_upstream(node, parent_map):
6
+ def find_upstream(node_ids: Iterable, parent_map):
7
7
  visited = set()
8
8
  upstream = set()
9
9
 
@@ -17,11 +17,13 @@ def find_upstream(node, parent_map):
17
17
  upstream.add(parent)
18
18
  dfs(parent)
19
19
 
20
- dfs(node)
20
+ for node_id in node_ids:
21
+ dfs(node_id)
22
+
21
23
  return upstream
22
24
 
23
25
 
24
- def find_downstream(node, child_map):
26
+ def find_downstream(node_ids: Iterable, child_map):
25
27
  visited = set()
26
28
  downstream = set()
27
29
 
@@ -35,13 +37,15 @@ def find_downstream(node, child_map):
35
37
  downstream.add(child)
36
38
  dfs(child)
37
39
 
38
- dfs(node)
40
+ for node_id in node_ids:
41
+ dfs(node_id)
42
+
39
43
  return downstream
40
44
 
41
45
 
42
46
  def find_column_dependencies(node_column_id: str, parent_map: Dict, child_map: Dict) -> Tuple[Set, Set]:
43
- upstream_cols = find_upstream(node_column_id, parent_map)
44
- downstream_cols = find_downstream(node_column_id, child_map)
47
+ upstream_cols = find_upstream([node_column_id], parent_map)
48
+ downstream_cols = find_downstream([node_column_id], child_map)
45
49
  return upstream_cols, downstream_cols
46
50
 
47
51
 
@@ -63,16 +67,16 @@ def filter_lineage_vertices(
63
67
 
64
68
 
65
69
  def filter_dependency_maps(
66
- parent_map: Dict, child_map: Dict, relevant_columns: Set
70
+ parent_map: Dict, child_map: Dict, relevant_ids: Set
67
71
  ) -> Tuple[Dict[str, Set], Dict[str, Set]]:
68
72
  p_map = {}
69
73
  c_map = {}
70
74
  for node_id, parents in parent_map.items():
71
- if node_id in relevant_columns:
72
- p_map[node_id] = {p for p in parents if p in relevant_columns}
75
+ if node_id in relevant_ids:
76
+ p_map[node_id] = {p for p in parents if p in relevant_ids}
73
77
 
74
78
  for node_id, children in child_map.items():
75
- if node_id in relevant_columns:
76
- c_map[node_id] = {c for c in children if c in relevant_columns}
79
+ if node_id in relevant_ids:
80
+ c_map[node_id] = {c for c in children if c in relevant_ids}
77
81
 
78
82
  return p_map, c_map
@@ -0,0 +1,45 @@
1
+ from enum import Enum
2
+ from typing import Union
3
+
4
+ from recce.util.recce_cloud import (
5
+ get_recce_cloud_onboarding_state,
6
+ set_recce_cloud_onboarding_state,
7
+ )
8
+
9
+
10
+ class OnboardingState(Enum):
11
+ SIGNUP_SUCCESSFUL = "signup_successful"
12
+ LAUNCHED_WITH_TOKEN = "launched_with_token"
13
+ CONFIGURE_TWO_ENV = "configure_two_env"
14
+
15
+
16
+ def update_onboarding_state(api_token: Union[str, bool, None], is_single_env: bool) -> OnboardingState:
17
+ if api_token:
18
+ # existing onboarding_state values -> new, launched, launched_with_two_envs
19
+ # new -> launched
20
+ # new -> launched_with_two_envs
21
+ # launched -> launched_with_two_envs
22
+ cloud_onboarding_state = get_recce_cloud_onboarding_state(api_token)
23
+
24
+ if cloud_onboarding_state == OnboardingState.SIGNUP_SUCCESSFUL.value:
25
+ # User has an API Token and is a "new" user
26
+ if is_single_env:
27
+ # Mark the onboarding state as "launched" if the user is new
28
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.LAUNCHED_WITH_TOKEN.value)
29
+ return OnboardingState.LAUNCHED_WITH_TOKEN
30
+ else:
31
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.CONFIGURE_TWO_ENV.value)
32
+ return OnboardingState.CONFIGURE_TWO_ENV
33
+ elif cloud_onboarding_state == OnboardingState.LAUNCHED_WITH_TOKEN.value:
34
+ # User has an API Token and has Two Environments
35
+ if is_single_env:
36
+ # Just return the current state
37
+ return OnboardingState.LAUNCHED_WITH_TOKEN
38
+ else:
39
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.CONFIGURE_TWO_ENV.value)
40
+ return OnboardingState.CONFIGURE_TWO_ENV
41
+ elif cloud_onboarding_state == OnboardingState.CONFIGURE_TWO_ENV.value:
42
+ # Just return the current state
43
+ return OnboardingState.CONFIGURE_TWO_ENV
44
+
45
+ return OnboardingState.SIGNUP_SUCCESSFUL
recce/util/recce_cloud.py CHANGED
@@ -180,13 +180,13 @@ class RecceCloud:
180
180
 
181
181
  def set_onboarding_state(self, state: str):
182
182
  api_url = f"{self.base_url}/users/onboarding-state"
183
- response = self._request("PUT", api_url, json={"state": state})
184
- if response.status_code != 200:
185
- raise RecceCloudException(
186
- message="Failed to update onboarding state in Recce Cloud.",
187
- reason=response.text,
188
- status_code=response.status_code,
189
- )
183
+ try:
184
+ response = self._request("PUT", api_url, json={"state": state})
185
+ response.raise_for_status()
186
+ except requests.exceptions.HTTPError as e:
187
+ # Don't Raise an exception if setting onboarding_state fails
188
+ logger.warning(f"Failed to set Onboarding State in Recce Cloud. Reason: {str(e)}")
189
+ return
190
190
 
191
191
 
192
192
  def get_recce_cloud_onboarding_state(token: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: recce-nightly
3
- Version: 1.9.0.20250622
3
+ Version: 1.9.0.20250624
4
4
  Summary: Environment diff tool for dbt
5
5
  Home-page: https://github.com/InfuseAI/recce
6
6
  Author: InfuseAI Dev Team
@@ -1,8 +1,9 @@
1
- recce/VERSION,sha256=Ko4z6NZFs-uR9cVLSn1qtggpU1S5lipxndZ_bYd4hgw,15
1
+ recce/VERSION,sha256=PM9mUjBu33bctG1WGN9EvPjwMgQRl2NheT822LUdTlc,15
2
2
  recce/__init__.py,sha256=yNb0QT-yoStex0VZALNJvUwtPLommoVCStcow31guqo,2392
3
3
  recce/artifact.py,sha256=tKQAHSrLRjiR3ppOI4sym8SxYiiLTuD3DPMYh4DWQdA,6506
4
- recce/cli.py,sha256=bhnVGR7K6cU3tyP-GbX2QHJgi4gFX_5xH4XqKWOOSY0,39710
4
+ recce/cli.py,sha256=4uFTt2vWlLikySC2TsV3j_obUyfeg0L2vEW1cqXQprY,40529
5
5
  recce/config.py,sha256=fs22mpFj8CFIxftGbhFAV5xIsPLX2xNTwWSer3UYn5k,4658
6
+ recce/connect_to_cloud.py,sha256=_SkX2pdyXa9FNyaHvItyYVPF2nZxy2JnCyd_o_psh2Y,4750
6
7
  recce/core.py,sha256=3Nv2QWBGz3yj1QI0wyPNRaDj1rH80DsX2qRQn9ydpg0,10747
7
8
  recce/diff.py,sha256=L2_bzQ3__PO-0aeir8PHF8FvSOUmQ8WcDXgML1-mHdY,748
8
9
  recce/exceptions.py,sha256=SclQ678GrHGjw7p_ZFJ3vZaL_yMU5xABeIAm2u_W2bk,592
@@ -10,25 +11,26 @@ recce/git.py,sha256=8Eg-6NzL-KjA3rT-ibbAyaCwGlzV0JqH3yGikrJNMDA,2344
10
11
  recce/github.py,sha256=PEpM6ZRiinsPbXSWj4aJCKbZrN1jUXzpzAfJq_CGah4,7420
11
12
  recce/pull_request.py,sha256=aW0B1NE2LUKTam1S4TQ7smXB9KLE1DV8GnyBqNXA6j8,3832
12
13
  recce/run.py,sha256=LAjbWUF8loZ9cL25d_maQELwROHHmfaFn7iqFRy5O1o,13567
13
- recce/server.py,sha256=Nr5y4jTk4CnHCTtcPHdJkWXjRybmSYBFAmxJ1t74Zlg,20667
14
- recce/state.py,sha256=VPzKyEdmyBeZ45BQArB587QN5C3Sfr7O5Oh_JRBcKrc,30616
14
+ recce/server.py,sha256=RFbZQP9RAsbtGEgtP-ltLc9ZvBZZM6T9qEn9nIwh3HY,20944
15
+ recce/state.py,sha256=fOzy09rn96XqOQuA0RcMW-k2KpjNVH9FQhihEXLvlA0,30634
15
16
  recce/summary.py,sha256=Mbxvxr9KazR5o9icqhhjiGHsoAiWxQU4PdN7HytBJ1c,19154
16
17
  recce/adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  recce/adapter/base.py,sha256=T_JNeLHgiHSaegw-DbrvHOaYjMyZcjj2Qtg5cWh_fco,3548
18
19
  recce/adapter/sqlmesh_adapter.py,sha256=IU3N-F6ToDoO7_bV5vsG8pmTuDcbFtewTIuCxedTaRM,5046
19
- recce/adapter/dbt_adapter/__init__.py,sha256=Bnh-6ZRO5k1yxJxjGJcf50NcMMTe6or2DoLaeFFMaZM,60211
20
+ recce/adapter/dbt_adapter/__init__.py,sha256=TqxYCxIe675XswFmx57hW55GV-YgC9pGMZW4mikVvV4,62852
20
21
  recce/adapter/dbt_adapter/dbt_version.py,sha256=M7aedZIWslXnJsryK8Ki4OL_t2oAKxy4uE2pRwfWIkk,1228
21
22
  recce/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
23
  recce/apis/check_api.py,sha256=KMCXSMl1qqzx2jQgRqCrD4j_cY3EHBbM3H2-t-6saAU,6227
23
24
  recce/apis/check_func.py,sha256=gktbCcyk3WGvWRJJ-wDnwv7NrIny2nTHWLl1-kdiVRo,4183
24
25
  recce/apis/run_api.py,sha256=eOaxOxXDkH59uqGCd4blld7edavUx7JU_DCd2WAYrL8,3416
25
26
  recce/apis/run_func.py,sha256=6wC8TDU-h7TLr2VZH7HNsWaUVlQ9HBN5N_dwqfi4lMY,7440
26
- recce/data/404.html,sha256=BhzZcWhFy9Im2YToG8rlIVHaIqetOX-tXyW0nv3FhLk,26574
27
+ recce/data/404.html,sha256=ZSuzOByvWRvnqWk8gxsrw8_5FmBt0mvHqXbSowXEK70,26574
28
+ recce/data/auth_callback.html,sha256=2QnhHRc2eLPiaxCYl-HndNsGDGmM6jZAMbqoMlehal4,94783
27
29
  recce/data/favicon.ico,sha256=B2mBumUOnzvUrXrqNkrc5QfdDXjzEXRcWkWur0fJ6sM,2565
28
- recce/data/index.html,sha256=sYlLEVgPoApkCbCFQd7r4MUJ0XWEi61y_Gg9NP_AsFQ,42155
29
- recce/data/index.txt,sha256=Q68kY89E_Iqmx1OSXbO9-8OxKFacmn5m5Jx5oR5Cq4Q,4462
30
- recce/data/_next/static/M3_t2h-wA4_IXOtKHdJqV/_buildManifest.js,sha256=sjy6FmOqWlkXWmod2oXwqKv7L582_MTycs9THCE6G-c,224
31
- recce/data/_next/static/M3_t2h-wA4_IXOtKHdJqV/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
30
+ recce/data/index.html,sha256=MybT9NbtYA-o_HIVCKS6agiqjoATOg2BFI19JZCBcvs,42155
31
+ recce/data/index.txt,sha256=f5XOwdv9lEn96-mYbI4hur7bUvreWiTlQX1vdf43l9o,4462
32
+ recce/data/_next/static/Iga3yMdHBVJ_Vhluh2SdE/_buildManifest.js,sha256=sjy6FmOqWlkXWmod2oXwqKv7L582_MTycs9THCE6G-c,224
33
+ recce/data/_next/static/Iga3yMdHBVJ_Vhluh2SdE/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
32
34
  recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js,sha256=2zRVkcaI906lYW_aM2mNqnt2HbLxmUaYFjft7eNcuTQ,310191
33
35
  recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js,sha256=wTH-_PbE1XdP-wmGaQStF5y2yH6XcZXeYX_hDaGMfh4,153777
34
36
  recce/data/_next/static/chunks/217-879a84d70f7a907c.js,sha256=kNJkLcwaIlWnQY7akbrFimbcHHdK6lMVTbTYCNKXIjc,124102
@@ -45,7 +47,7 @@ recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js,sha256=NfgBK9op5SzWm
45
47
  recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js,sha256=ewMLioJbTUUDeqV0J9NDKE0Bj-XCFg8QqMLOmLPgpcc,777
46
48
  recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js,sha256=qKkzgWsNqSrn-PUGBloZopIlacGCy8hH0BZA9tpqQoQ,172837
47
49
  recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js,sha256=NPGWvqv8Y_rbNaKxYnoq16mi6FDoQHxOOjpH4tJnUkE,119972
48
- recce/data/_next/static/chunks/92-7ab55ae02606193c.js,sha256=f0YhpleWOSyqlxfS32YC25CQnb0EJAujrN4BWncRKdY,80497
50
+ recce/data/_next/static/chunks/92-607cd1af83c41f43.js,sha256=0ThWZlPjuwxRRFkwB0qN9Q6QdMxJzdGhuIyLAOHO4lw,80466
49
51
  recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js,sha256=lV-V6BYpd4HEt-pFtT03G6qQD2Wa-2L_jp6aH9VgoeM,4177
50
52
  recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js,sha256=qzgyjrX5PdPKGeSwb-Gh0RfM_NEF2zjMvMK14ixq228,864
51
53
  recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js,sha256=s2EZ59zDBJDmy2kh0cHkQ_pAWxiQr_yCPlnttCj0x_k,572
@@ -60,7 +62,7 @@ recce/data/_next/static/chunks/main-b5b3ae20a1405261.js,sha256=28hJH2VsiTdxZGZrQ
60
62
  recce/data/_next/static/chunks/polyfills-42372ed130431b0a.js,sha256=CXPB1kyIrcjjyVBBDLWLKI9yEY1ZZbeASUON648vloM,112594
61
63
  recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js,sha256=lcK59Juk4EZqi3jIsY35lQqAlB5TLTPy-neri0BIZL0,3929
62
64
  recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js,sha256=Hmy6Lmrt4GPYYa0f1qhxTiMj4K47PCmkAkrm1Rbnd7w,1731
63
- recce/data/_next/static/chunks/app/page-707b8e8d98d7f780.js,sha256=4R2WXYq5x46aRuNVKckw4f5YmkbU5XI0fFSOeEhGt_E,156467
65
+ recce/data/_next/static/chunks/app/page-3f79c2e6d6f03a09.js,sha256=N3h3lkuP_o97aLrOd0lZp--oJqjuw_SOv733KWqOCM0,156413
64
66
  recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js,sha256=_l3JSAaIJxInWqyqdDsobkhJ3JP6DZuwXkw-MsRakQA,1751
65
67
  recce/data/_next/static/chunks/pages/_app-437c455677d62394.js,sha256=FaxURD1wuAmnMU1zJlsQKT9rRdfuspR7yx7BckcoCLY,284
66
68
  recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js,sha256=0Vzx_hCqw4RcptsFzHfUigpI13sToBnFw38owTP7ruo,250
@@ -91,7 +93,7 @@ recce/event/track.py,sha256=xDyDWblhR6mp0tezEYhkA0aOyZLqXBpJyHZwhI2xhU8,4803
91
93
  recce/models/__init__.py,sha256=F7cgALtdWnwv37R0eEgKZ_yBsMwxWnUfo3jAZ3u6qyU,209
92
94
  recce/models/check.py,sha256=jjR1SGyczjrqyK-0Zas6ikLIGSgVp54lvfcQA19AniI,1588
93
95
  recce/models/run.py,sha256=QK2gvOWvko9YYhd2NLs3BPt5l4MSCZGwpzTAiqx9zJw,1161
94
- recce/models/types.py,sha256=g4w5RNNeeNRUDyLWd1Sqv7wPvt0ue4d0-_-KZ585GJI,4193
96
+ recce/models/types.py,sha256=0YbhuL_MiDinhtqq9UQJQYu5_zTNWOjm7G5t-WKbDV0,4390
95
97
  recce/tasks/__init__.py,sha256=b553AtDHjYROgmMePv_Hv_X3fjh4iEn11gzzpUJz6_o,610
96
98
  recce/tasks/core.py,sha256=JFYa1CfgOiRPQ7KVTwMuxJjhMB-pvCwB-xezVt-h3RU,4080
97
99
  recce/tasks/dataframe.py,sha256=03UBWwt0DFTXlaEOtnV5i_mxdRKD7UbRayewEL2Ub48,3650
@@ -104,21 +106,23 @@ recce/tasks/schema.py,sha256=HHrSvhd_ZJdrNj2QKi9W8vmig0NuYci5cgsKFvp2bu4,2274
104
106
  recce/tasks/top_k.py,sha256=vY3VCBmg61E8I4V-9-hJOv5RCYCmhxl6sHiKR9Zv7eE,5521
105
107
  recce/tasks/valuediff.py,sha256=XJWkA307B5qTerT87fJRhJxPCqjmXn5eILYCXDfPtSQ,16439
106
108
  recce/util/__init__.py,sha256=cDvL3WT32cJR9CUPIJmibwJoyYcB3ZaKBFbOZDyvqP8,95
107
- recce/util/api_token.py,sha256=w_NI4pzcRv2FpBkYPW9Bg9TtQa50pB-hV1ukriIrQdg,3102
109
+ recce/util/api_token.py,sha256=D1fM5romXnxXIvmedr8_0r_jspRRjWsmDK-lM7VSrk4,2737
108
110
  recce/util/breaking.py,sha256=evT-xlDWMgIbuPQH6w5yNoDjOqopKzBXnCF9_xTn1S0,12591
109
111
  recce/util/cache.py,sha256=QB6wzxe0M3jNTwP0M27Ys8F2hF-oda4-LyXXG9THuZQ,646
110
112
  recce/util/cll.py,sha256=VXhtyoPIcPEW3_SmNUTbZWwOmmv8_JRfAB9CLJ4S2Wg,13315
111
113
  recce/util/io.py,sha256=fNiIafNxPlTF6NNXlugzeJjfVGGrP3RXb39ERzRWD3Q,3459
112
- recce/util/lineage.py,sha256=IF1CNrYt0u_bNkB4XQaueteD1JqGKDEnNQV-Qba26ZE,2090
114
+ recce/util/lineage.py,sha256=WdHxFTViqykBT8hBIbhGOovpEVnu9hbaW0VEQJUiCmE,2186
113
115
  recce/util/logger.py,sha256=6UgLFkRiur9jJfu2ZRdo4LUvMw4f75V-l-1HT1-sgKo,747
116
+ recce/util/onboarding_state.py,sha256=kwFirKlfXdl5WFkR_nmilqGKFyELNcSPMqYq-by35fk,1991
114
117
  recce/util/pydantic_model.py,sha256=KumKuyCjbTzEMsKLE4-b-eZfp0gLhYDdmVtw1-hxiJw,587
115
- recce/util/recce_cloud.py,sha256=g7uhWkknR9ulTuGxaz14erOGiUwIv3q3C0mfCEG3gl8,7925
118
+ recce/util/recce_cloud.py,sha256=YgRs9PhER3EhESaJInJo6mf3uwvnp0OavHBY1OYHPXw,7958
116
119
  recce/util/singleton.py,sha256=1cU99I0f9tjuMQLMJyLsK1oK3fZJMsO5-TbRHAMXqds,627
117
120
  recce/yaml/__init__.py,sha256=EgXYlFeJZchatUClRDXbIC5Oqb2_nBvB2NqItYVihio,1292
118
- recce_nightly-1.9.0.20250622.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
121
+ recce_nightly-1.9.0.20250624.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
119
122
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
123
  tests/test_cli.py,sha256=_ikuZW4bYEGyj5A_V7nDSyBa1tVZqc0TVm-TJ0rEYVo,5467
121
124
  tests/test_config.py,sha256=ODDFe_XF6gphmSmmc422dGLBaCCmG-IjDzTkD5SJsJE,1557
125
+ tests/test_connect_to_cloud.py,sha256=b2fgV8L1iQBdEwh6RumMsIIyYg7GtMOMRz1dvE3WRPg,3059
122
126
  tests/test_core.py,sha256=jfn9dV-k89O2TywUYHmMTSdvyfBUips1ful1cIY2oFY,799
123
127
  tests/test_dbt.py,sha256=VzXvdoJNwwEmKNhJJDNB1N_qZYwp1aoJQ1sLcoyRBmk,1316
124
128
  tests/test_pull_request.py,sha256=HmZo5MoDaoKSgPwbLxJ3Ur3ajZ7IxhkzJxaOmhg6bwE,3562
@@ -130,7 +134,7 @@ tests/adapter/dbt_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
130
134
  tests/adapter/dbt_adapter/conftest.py,sha256=WlMVytJcyfLJlMBeYhpk8Q-nyCDR9x_7qgsy6Uh9ECw,386
131
135
  tests/adapter/dbt_adapter/dbt_test_helper.py,sha256=o_7m0cnVeGwPwEFXBCx3Me5QqLzQMOBR3dXD-MS2eaM,10406
132
136
  tests/adapter/dbt_adapter/test_dbt_adapter.py,sha256=Y5TSlqkXGjJC6hC7YYs45bnRht5aou_KLjPMjpAGxNM,830
133
- tests/adapter/dbt_adapter/test_dbt_cll.py,sha256=W3VKg6GKUMGyR_qomwbOpVcSyOTpYBb_KMhS4NfGvS8,14249
137
+ tests/adapter/dbt_adapter/test_dbt_cll.py,sha256=IvFIImI6SQF-F-Ti0tHvUEZkAiKOmdpb9ZeJ_5JOw9w,14294
134
138
  tests/adapter/dbt_adapter/test_selector.py,sha256=uKFm0QxrxG-xyUEXGobCMueCRUssgnZMyo-S1Nlk3_s,6856
135
139
  tests/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
140
  tests/tasks/conftest.py,sha256=lyZFx4jEY3To3xVhfMQ-G45egYOCttKN2lxeyExACkY,157
@@ -143,8 +147,8 @@ tests/tasks/test_row_count.py,sha256=21PaP2aq-x8-pqwzWHRT1sixhQ8g3CQNRWOZTTmbK0s
143
147
  tests/tasks/test_schema.py,sha256=7ds4Vx8ixaiIWDR49Lvjem4xlPkRP1cXazDRY3roUak,3121
144
148
  tests/tasks/test_top_k.py,sha256=YR_GS__DJsbDlQVaEEdJvNQ3fh1VmV5Nb3G7lb0r6YM,1779
145
149
  tests/tasks/test_valuediff.py,sha256=_xQJGgxsXoy2NYk_Z6Hsw2FlVh6zk2nN_iUueyRN1e8,2046
146
- recce_nightly-1.9.0.20250622.dist-info/METADATA,sha256=FrkMZ6YqX3pe92_YA77jSUcNGl1gAYiNACnMlt7ztl0,9397
147
- recce_nightly-1.9.0.20250622.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
148
- recce_nightly-1.9.0.20250622.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
149
- recce_nightly-1.9.0.20250622.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
150
- recce_nightly-1.9.0.20250622.dist-info/RECORD,,
150
+ recce_nightly-1.9.0.20250624.dist-info/METADATA,sha256=okPHlV3RrR0VRdIJj1C7cXeoRRADck7xIb4fhNbsuDc,9397
151
+ recce_nightly-1.9.0.20250624.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
152
+ recce_nightly-1.9.0.20250624.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
153
+ recce_nightly-1.9.0.20250624.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
154
+ recce_nightly-1.9.0.20250624.dist-info/RECORD,,
@@ -51,11 +51,11 @@ def test_cll_basic(dbt_test_helper):
51
51
  )
52
52
  adapter: DbtAdapter = dbt_test_helper.context.adapter
53
53
 
54
- result = adapter.get_cll_by_node_id("model.model2")
54
+ result = adapter.get_cll("model.model2", no_filter=True)
55
55
  assert_model(result, "model.model2", [("model.model1", "c")])
56
56
  assert_column(result, "model.model2", "c", "passthrough", [("model.model1", "c")])
57
57
 
58
- result = adapter.get_cll_by_node_id("model.model3")
58
+ result = adapter.get_cll("model.model3", no_filter=True)
59
59
  assert_model(result, "model.model3", [("model.model1", "c")])
60
60
  assert_column(result, "model.model3", "c", "passthrough", [("model.model1", "c")])
61
61
 
@@ -75,7 +75,7 @@ def test_cll_table_alisa(dbt_test_helper):
75
75
  depends_on=["model.model1"],
76
76
  )
77
77
  adapter: DbtAdapter = dbt_test_helper.context.adapter
78
- result = adapter.get_cll_by_node_id("model.model1")
78
+ result = adapter.get_cll("model.model1", no_filter=True)
79
79
  assert_column(result, "model.model2", "c", "passthrough", [("model.model1", "c")])
80
80
 
81
81
 
@@ -103,7 +103,7 @@ def test_seed(dbt_test_helper):
103
103
  )
104
104
  adapter: DbtAdapter = dbt_test_helper.context.adapter
105
105
 
106
- result = adapter.get_cll_by_node_id("model.model1")
106
+ result = adapter.get_cll("model.model1", no_filter=True)
107
107
  assert_model(result, "seed.seed1", [])
108
108
  assert_column(result, "seed.seed1", "customer_id", "source", [])
109
109
  assert_model(result, "model.model1", [("seed.seed1", "age")])
@@ -134,7 +134,7 @@ def test_python_model(dbt_test_helper):
134
134
  assert not adapter.is_python_model("model1")
135
135
  assert adapter.is_python_model("model2")
136
136
 
137
- result = adapter.get_cll_by_node_id("model1")
137
+ result = adapter.get_cll("model1", no_filter=True)
138
138
  assert_column(result, "model2", "customer_id", "unknown", [])
139
139
 
140
140
 
@@ -161,9 +161,9 @@ def test_source(dbt_test_helper):
161
161
  depends_on=["source.source1.table1"],
162
162
  )
163
163
  adapter: DbtAdapter = dbt_test_helper.context.adapter
164
- result = adapter.get_cll_by_node_id("source.source1.table1")
164
+ result = adapter.get_cll("source.source1.table1", no_filter=True)
165
165
  assert_column(result, "source.source1.table1", "customer_id", "source", [])
166
- result = adapter.get_cll_by_node_id("model.model1")
166
+ result = adapter.get_cll("model.model1", no_filter=True)
167
167
  assert_column(result, "model.model1", "customer_id", "passthrough", [("source.source1.table1", "customer_id")])
168
168
 
169
169
 
@@ -171,14 +171,14 @@ def test_parse_error(dbt_test_helper):
171
171
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c", curr_columns={"c": "int"})
172
172
  dbt_test_helper.create_model("model2", curr_sql="this is not a valid sql", curr_columns={"c": "int"})
173
173
  adapter: DbtAdapter = dbt_test_helper.context.adapter
174
- result = adapter.get_cll_by_node_id("model2")
174
+ result = adapter.get_cll("model2", no_filter=True)
175
175
  assert_column(result, "model2", "c", "unknown", [])
176
176
 
177
177
 
178
178
  def test_model_without_catalog(dbt_test_helper):
179
179
  dbt_test_helper.create_model("model1", curr_sql="select 1 as c")
180
180
  adapter: DbtAdapter = dbt_test_helper.context.adapter
181
- result = adapter.get_cll_by_node_id("model1")
181
+ result = adapter.get_cll("model1", no_filter=True)
182
182
  assert not result.nodes["model1"].columns
183
183
 
184
184
 
@@ -0,0 +1,82 @@
1
+ import base64
2
+ import unittest
3
+ from unittest.mock import patch
4
+ from urllib.parse import quote
5
+
6
+ from cryptography.hazmat.primitives import hashes
7
+ from cryptography.hazmat.primitives.asymmetric import padding
8
+
9
+ from recce.connect_to_cloud import (
10
+ connect_to_cloud_background_task,
11
+ decrypt_code,
12
+ generate_key_pair,
13
+ is_callback_server_running,
14
+ prepare_connection_url,
15
+ )
16
+
17
+
18
+ class ConnectToCloudTests(unittest.TestCase):
19
+
20
+ def test_generate_key_pair(self):
21
+ private_key, public_key = generate_key_pair()
22
+ self.assertIsNotNone(private_key)
23
+ self.assertIsNotNone(public_key)
24
+ self.assertEqual(private_key.public_key().public_numbers(), public_key.public_numbers())
25
+
26
+ def test_prepare_connection_url(self):
27
+ _, public_key = generate_key_pair()
28
+ url, port = prepare_connection_url(public_key)
29
+ self.assertIn("connect?", url)
30
+ self.assertTrue(port >= 10000 and port <= 15000)
31
+
32
+ def test_decrypt_code(self):
33
+ private_key, public_key = generate_key_pair()
34
+ test_string = "recce-api-token-123"
35
+ ciphertext = public_key.encrypt(
36
+ test_string.encode(),
37
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None),
38
+ )
39
+ b64_ciphertext = base64.b64encode(ciphertext).decode()
40
+ result = decrypt_code(private_key, b64_ciphertext)
41
+ self.assertEqual(result, test_string)
42
+
43
+ @patch("recce.connect_to_cloud.update_recce_api_token")
44
+ @patch("recce.connect_to_cloud.update_onboarding_state")
45
+ @patch("recce.connect_to_cloud.RecceCloud")
46
+ def test_handle_callback_request_success(self, mock_recce_cloud, mock_update_state, mock_update_token):
47
+ private_key, public_key = generate_key_pair()
48
+
49
+ # Prepare encrypted token
50
+ test_token = "recce-api-token-xyz"
51
+ ciphertext = public_key.encrypt(
52
+ test_token.encode(),
53
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None),
54
+ )
55
+ encrypted_b64 = base64.b64encode(ciphertext).decode()
56
+
57
+ # Set up mocks
58
+ mock_recce_cloud.return_value.verify_token.return_value = True
59
+
60
+ from recce.connect_to_cloud import handle_callback_request
61
+
62
+ result = handle_callback_request(f"code={quote(encrypted_b64)}", private_key)
63
+
64
+ assert result == test_token
65
+ mock_update_token.assert_called_once_with(test_token)
66
+ mock_update_state.assert_called_once_with(test_token, False)
67
+
68
+ def test_is_callback_server_running(self):
69
+ # Should return False by default
70
+ self.assertFalse(is_callback_server_running())
71
+
72
+ @patch("recce.connect_to_cloud.run_one_time_http_server")
73
+ def test_connect_to_cloud_background_task_runs(self, mock_server):
74
+ private_key, public_key = generate_key_pair()
75
+ url, port = prepare_connection_url(public_key)
76
+
77
+ connect_to_cloud_background_task(private_key, port, url)
78
+ mock_server.assert_called_once()
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()