recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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 (213) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +810 -480
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +119 -51
  12. recce/cli.py +1299 -323
  13. recce/config.py +42 -33
  14. recce/connect_to_cloud.py +138 -0
  15. recce/core.py +55 -47
  16. recce/data/404.html +1 -1
  17. recce/data/__next.__PAGE__.txt +10 -0
  18. recce/data/__next._full.txt +23 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +8 -0
  21. recce/data/__next._tree.txt +5 -0
  22. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  23. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  24. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  25. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  26. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  27. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  28. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  29. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  30. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  31. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  32. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  33. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  34. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  35. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  36. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  37. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  38. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  39. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  40. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  41. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  42. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  43. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  44. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  45. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  46. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  47. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  48. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  49. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  50. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  51. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  52. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  53. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  54. recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
  55. recce/data/_not-found/__next._full.txt +17 -0
  56. recce/data/_not-found/__next._head.txt +8 -0
  57. recce/data/_not-found/__next._index.txt +8 -0
  58. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  59. recce/data/_not-found/__next._not-found.txt +4 -0
  60. recce/data/_not-found/__next._tree.txt +3 -0
  61. recce/data/_not-found.html +1 -0
  62. recce/data/_not-found.txt +17 -0
  63. recce/data/auth_callback.html +68 -0
  64. recce/data/imgs/reload-image.svg +4 -0
  65. recce/data/index.html +1 -27
  66. recce/data/index.txt +23 -7
  67. recce/diff.py +6 -12
  68. recce/event/__init__.py +86 -74
  69. recce/event/collector.py +33 -22
  70. recce/event/track.py +49 -27
  71. recce/exceptions.py +1 -1
  72. recce/git.py +7 -7
  73. recce/github.py +57 -53
  74. recce/mcp_server.py +716 -0
  75. recce/models/__init__.py +4 -1
  76. recce/models/check.py +6 -7
  77. recce/models/run.py +1 -0
  78. recce/models/types.py +131 -28
  79. recce/pull_request.py +27 -25
  80. recce/run.py +165 -121
  81. recce/server.py +303 -111
  82. recce/state/__init__.py +31 -0
  83. recce/state/cloud.py +632 -0
  84. recce/state/const.py +26 -0
  85. recce/state/local.py +56 -0
  86. recce/state/state.py +119 -0
  87. recce/state/state_loader.py +174 -0
  88. recce/summary.py +188 -143
  89. recce/tasks/__init__.py +19 -3
  90. recce/tasks/core.py +11 -13
  91. recce/tasks/dataframe.py +82 -18
  92. recce/tasks/histogram.py +69 -34
  93. recce/tasks/lineage.py +2 -2
  94. recce/tasks/profile.py +152 -86
  95. recce/tasks/query.py +139 -87
  96. recce/tasks/rowcount.py +37 -31
  97. recce/tasks/schema.py +18 -15
  98. recce/tasks/top_k.py +35 -35
  99. recce/tasks/valuediff.py +216 -152
  100. recce/util/__init__.py +3 -0
  101. recce/util/api_token.py +80 -0
  102. recce/util/breaking.py +87 -85
  103. recce/util/cll.py +274 -219
  104. recce/util/io.py +22 -17
  105. recce/util/lineage.py +65 -16
  106. recce/util/logger.py +1 -1
  107. recce/util/onboarding_state.py +45 -0
  108. recce/util/perf_tracking.py +85 -0
  109. recce/util/recce_cloud.py +322 -72
  110. recce/util/singleton.py +4 -4
  111. recce/yaml/__init__.py +7 -10
  112. recce_cloud/__init__.py +24 -0
  113. recce_cloud/api/__init__.py +17 -0
  114. recce_cloud/api/base.py +111 -0
  115. recce_cloud/api/client.py +150 -0
  116. recce_cloud/api/exceptions.py +26 -0
  117. recce_cloud/api/factory.py +63 -0
  118. recce_cloud/api/github.py +76 -0
  119. recce_cloud/api/gitlab.py +82 -0
  120. recce_cloud/artifact.py +57 -0
  121. recce_cloud/ci_providers/__init__.py +9 -0
  122. recce_cloud/ci_providers/base.py +82 -0
  123. recce_cloud/ci_providers/detector.py +147 -0
  124. recce_cloud/ci_providers/github_actions.py +136 -0
  125. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  126. recce_cloud/cli.py +245 -0
  127. recce_cloud/upload.py +214 -0
  128. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
  129. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  130. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
  131. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  132. tests/adapter/dbt_adapter/conftest.py +9 -5
  133. tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
  134. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  135. tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
  136. tests/adapter/dbt_adapter/test_selector.py +22 -21
  137. tests/recce_cloud/__init__.py +0 -0
  138. tests/recce_cloud/test_ci_providers.py +351 -0
  139. tests/recce_cloud/test_cli.py +372 -0
  140. tests/recce_cloud/test_client.py +273 -0
  141. tests/recce_cloud/test_platform_clients.py +333 -0
  142. tests/tasks/conftest.py +1 -1
  143. tests/tasks/test_histogram.py +58 -66
  144. tests/tasks/test_lineage.py +36 -23
  145. tests/tasks/test_preset_checks.py +45 -31
  146. tests/tasks/test_profile.py +339 -15
  147. tests/tasks/test_query.py +46 -46
  148. tests/tasks/test_row_count.py +65 -46
  149. tests/tasks/test_schema.py +65 -42
  150. tests/tasks/test_top_k.py +22 -18
  151. tests/tasks/test_valuediff.py +43 -32
  152. tests/test_cli.py +174 -60
  153. tests/test_cli_mcp_optional.py +45 -0
  154. tests/test_cloud_listing_cli.py +324 -0
  155. tests/test_config.py +7 -9
  156. tests/test_connect_to_cloud.py +82 -0
  157. tests/test_core.py +151 -4
  158. tests/test_dbt.py +7 -7
  159. tests/test_mcp_server.py +332 -0
  160. tests/test_pull_request.py +1 -1
  161. tests/test_server.py +25 -19
  162. tests/test_summary.py +29 -17
  163. recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
  164. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  165. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  166. recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
  167. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  168. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  169. recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
  170. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  171. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  172. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  173. recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
  174. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  175. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  176. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  177. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  178. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  179. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  180. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  181. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  182. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  183. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  184. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  185. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  186. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  187. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  188. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  189. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  190. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  191. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  192. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  193. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  194. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  195. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  196. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  197. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  198. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  199. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  200. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  202. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  203. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  205. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  206. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  207. recce/state.py +0 -753
  208. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  209. tests/test_state.py +0 -123
  210. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  211. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  212. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  213. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/event/__init__.py CHANGED
@@ -10,17 +10,21 @@ from typing import Dict
10
10
 
11
11
  import sentry_sdk
12
12
 
13
- from recce import is_ci_env, get_version, get_runner
13
+ from recce import get_runner, get_version, is_ci_env, is_recce_cloud_instance
14
14
  from recce import yaml as pyml
15
15
  from recce.event.collector import Collector
16
16
  from recce.git import current_branch, hosting_repo
17
- from recce.github import is_github_codespace, get_github_codespace_info, get_github_codespace_name, \
18
- get_github_codespace_available_at
19
-
20
- USER_HOME = os.path.expanduser('~')
21
- RECCE_USER_HOME = os.path.join(USER_HOME, '.recce')
22
- RECCE_USER_PROFILE = os.path.join(RECCE_USER_HOME, 'profile.yml')
23
- RECCE_USER_EVENT_PATH = os.path.join(RECCE_USER_HOME, '.unsend_events.json')
17
+ from recce.github import (
18
+ get_github_codespace_available_at,
19
+ get_github_codespace_info,
20
+ get_github_codespace_name,
21
+ is_github_codespace,
22
+ )
23
+
24
+ USER_HOME = os.path.expanduser("~")
25
+ RECCE_USER_HOME = os.path.join(USER_HOME, ".recce")
26
+ RECCE_USER_PROFILE = os.path.join(RECCE_USER_HOME, "profile.yml")
27
+ RECCE_USER_EVENT_PATH = os.path.join(RECCE_USER_HOME, ".unsend_events.json")
24
28
 
25
29
  __version__ = get_version()
26
30
  _collector = Collector()
@@ -33,13 +37,13 @@ def init():
33
37
 
34
38
  # Amplitude init
35
39
  _collector.set_api_key(api_key)
36
- _collector.set_user_id(user_profile.get('user_id'))
40
+ _collector.set_user_id(user_profile.get("user_id"))
37
41
  _collector.set_unsend_events_file(RECCE_USER_EVENT_PATH)
38
42
 
39
43
  # Sentry init
40
44
  sentry_env = _get_sentry_env()
41
45
  sentry_dns = _get_sentry_dns()
42
- release_version = __version__ if sentry_env != 'development' else None
46
+ release_version = __version__ if sentry_env != "development" else None
43
47
  sentry_sdk.init(
44
48
  dsn=sentry_dns,
45
49
  environment=sentry_env,
@@ -47,58 +51,63 @@ def init():
47
51
  # Set traces_sample_rate to 1.0 to capture 100%
48
52
  # of transactions for performance monitoring.
49
53
  # We recommend adjusting this value in production.
50
- traces_sample_rate=1.0
54
+ traces_sample_rate=1.0,
51
55
  )
52
- sentry_sdk.set_tag('recce.version', __version__)
53
- sentry_sdk.set_tag('platform', sys.platform)
54
- sentry_sdk.set_tag('is_ci_env', is_ci_env())
55
- sentry_sdk.set_tag('is_github_codespace', is_github_codespace())
56
- sentry_sdk.set_tag('system_timezone', get_system_timezone())
56
+ sentry_sdk.set_tag("recce.version", __version__)
57
+ sentry_sdk.set_tag("platform", sys.platform)
58
+ sentry_sdk.set_tag("is_ci_env", is_ci_env())
59
+ sentry_sdk.set_tag("is_github_codespace", is_github_codespace())
60
+ sentry_sdk.set_tag("is_recce_cloud_instance", is_recce_cloud_instance())
61
+ sentry_sdk.set_tag("system_timezone", get_system_timezone())
57
62
 
58
63
 
59
64
  def get_user_id():
60
- return load_user_profile().get('user_id')
65
+ return load_user_profile().get("user_id")
61
66
 
62
67
 
63
68
  def get_recce_api_token():
64
- return load_user_profile().get('api_token')
69
+ return load_user_profile().get("api_token")
70
+
71
+
72
+ def update_recce_api_token(token):
73
+ return update_user_profile({"api_token": token})
65
74
 
66
75
 
67
76
  def is_anonymous_tracking():
68
- return load_user_profile().get('anonymous_tracking', False)
77
+ return load_user_profile().get("anonymous_tracking", False)
69
78
 
70
79
 
71
80
  def _get_sentry_dns():
72
- dns_file = os.path.normpath(os.path.join(os.path.dirname(__file__), 'SENTRY_DNS'))
73
- with open(dns_file) as f:
81
+ dns_file = os.path.normpath(os.path.join(os.path.dirname(__file__), "SENTRY_DNS"))
82
+ with open(dns_file, encoding="utf-8") as f:
74
83
  dns = f.read().strip()
75
84
  return dns
76
85
 
77
86
 
78
87
  def _get_sentry_env():
79
- if '.dev' in __version__:
80
- return 'development'
81
- elif re.match(r'^\d+\.\d+\.\d+\.\d{8}[a|b|rc]?.*$', __version__):
82
- return 'nightly'
83
- elif 'a' in __version__:
84
- return 'alpha'
85
- elif 'b' in __version__:
86
- return 'beta'
87
- elif 'rc' in __version__:
88
- return 'release-candidate'
89
- return 'production'
88
+ if ".dev" in __version__:
89
+ return "development"
90
+ elif re.match(r"^\d+\.\d+\.\d+\.\d{8}[a|b|rc]?.*$", __version__):
91
+ return "nightly"
92
+ elif "a" in __version__:
93
+ return "alpha"
94
+ elif "b" in __version__:
95
+ return "beta"
96
+ elif "rc" in __version__:
97
+ return "release-candidate"
98
+ return "production"
90
99
 
91
100
 
92
101
  def _get_api_key():
93
- if os.getenv('RECCE_EVENT_API_KEY'):
102
+ if os.getenv("RECCE_EVENT_API_KEY"):
94
103
  # For local testing purpose
95
- return os.getenv('RECCE_EVENT_API_KEY')
104
+ return os.getenv("RECCE_EVENT_API_KEY")
96
105
 
97
- config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'CONFIG'))
106
+ config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "CONFIG"))
98
107
  try:
99
- with open(config_file) as fh:
108
+ with open(config_file, encoding="utf-8") as fh:
100
109
  config = pyml.load(fh)
101
- return config.get('event_api_key')
110
+ return config.get("event_api_key")
102
111
  except Exception:
103
112
  return None
104
113
 
@@ -108,15 +117,15 @@ def _generate_user_profile():
108
117
  os.makedirs(RECCE_USER_HOME, exist_ok=True)
109
118
  except Exception:
110
119
  # TODO: should show warning message but not raise exception
111
- print('Please disable command tracking to continue.')
120
+ print("Please disable command tracking to continue.")
112
121
  exit(1)
113
122
  if is_github_codespace() is True:
114
- salted_name = f'codespace-{get_github_codespace_name()}'
123
+ salted_name = f"codespace-{get_github_codespace_name()}"
115
124
  user_id = hashlib.sha256(salted_name.encode()).hexdigest()
116
125
  else:
117
126
  user_id = uuid.uuid4().hex
118
- with open(RECCE_USER_PROFILE, 'w+') as f:
119
- pyml.dump({'user_id': user_id, 'anonymous_tracking': True}, f)
127
+ with open(RECCE_USER_PROFILE, "w+", encoding="utf-8") as f:
128
+ pyml.dump({"user_id": user_id, "anonymous_tracking": True}, f)
120
129
  return dict(user_id=user_id, anonymous_tracking=True)
121
130
 
122
131
 
@@ -125,9 +134,9 @@ def load_user_profile():
125
134
  if not os.path.exists(RECCE_USER_PROFILE):
126
135
  user_profile = _generate_user_profile()
127
136
  else:
128
- with open(RECCE_USER_PROFILE, 'r') as f:
137
+ with open(RECCE_USER_PROFILE, "r", encoding="utf-8") as f:
129
138
  user_profile = pyml.load(f)
130
- if user_profile.get('user_id') is None:
139
+ if user_profile is None or user_profile.get("user_id") is None:
131
140
  user_profile = _generate_user_profile()
132
141
 
133
142
  return user_profile
@@ -136,7 +145,7 @@ def load_user_profile():
136
145
  def update_user_profile(update_values):
137
146
  original = load_user_profile()
138
147
  original.update(update_values)
139
- with open(RECCE_USER_PROFILE, 'w+') as f:
148
+ with open(RECCE_USER_PROFILE, "w+", encoding="utf-8") as f:
140
149
  pyml.dump(original, f)
141
150
  return original
142
151
 
@@ -146,10 +155,10 @@ def flush_events(command=None):
146
155
 
147
156
 
148
157
  def should_log_event():
149
- with open(RECCE_USER_PROFILE, 'r') as f:
158
+ with open(RECCE_USER_PROFILE, "r", encoding="utf-8") as f:
150
159
  user_profile = pyml.load(f)
151
160
  # TODO: default anonymous_tracking to false if field is not present
152
- tracking = user_profile.get('anonymous_tracking', False)
161
+ tracking = user_profile.get("anonymous_tracking", False)
153
162
  tracking = tracking and isinstance(tracking, bool)
154
163
  if not tracking:
155
164
  return False
@@ -166,18 +175,18 @@ def log_event(prop, event_type, **kwargs):
166
175
 
167
176
  repo = hosting_repo()
168
177
  if repo is not None:
169
- prop['repository'] = sha256(repo.encode()).hexdigest()
178
+ prop["repository"] = sha256(repo.encode()).hexdigest()
170
179
 
171
180
  branch = current_branch()
172
181
  if branch is not None:
173
- prop['branch'] = sha256(branch.encode()).hexdigest()
182
+ prop["branch"] = sha256(branch.encode()).hexdigest()
174
183
 
175
184
  runner = get_runner()
176
185
  if runner is not None:
177
- prop['runner_type'] = runner
186
+ prop["runner_type"] = runner
178
187
 
179
- if runner == 'github codespaces':
180
- prop['codespaces_name'] = get_github_codespace_name()
188
+ if runner == "github codespaces":
189
+ prop["codespaces_name"] = get_github_codespace_name()
181
190
 
182
191
  payload = dict(
183
192
  **prop,
@@ -191,11 +200,11 @@ def log_api_event(endpoint_name, prop):
191
200
  **prop,
192
201
  endpoint_name=endpoint_name,
193
202
  )
194
- log_event(prop, 'api_event')
203
+ log_event(prop, "api_event")
195
204
  _collector.schedule_flush()
196
205
 
197
206
 
198
- def log_load_state(command='server', single_env=False):
207
+ def log_load_state(command="server", single_env=False):
199
208
  from recce.models import CheckDAO
200
209
 
201
210
  checks = 0
@@ -213,10 +222,10 @@ def log_load_state(command='server', single_env=False):
213
222
  )
214
223
 
215
224
  if command == "server":
216
- prop['single_env'] = single_env
225
+ prop["single_env"] = single_env
217
226
 
218
- log_event(prop, 'load_state')
219
- if command == 'server':
227
+ log_event(prop, "load_state")
228
+ if command == "server":
220
229
  _collector.schedule_flush()
221
230
 
222
231
 
@@ -227,31 +236,29 @@ def log_codespaces_events(command):
227
236
  return
228
237
 
229
238
  user_prop = dict(
230
- location=codespace.get('location'),
231
- is_prebuild=codespace.get('prebuild', False),
239
+ location=codespace.get("location"),
240
+ is_prebuild=codespace.get("prebuild", False),
232
241
  )
233
242
 
234
243
  prop = dict(
235
- machine=codespace.get('machine', {}).get('display_name'),
244
+ machine=codespace.get("machine", {}).get("display_name"),
236
245
  codespaces_name=get_github_codespace_name(),
237
246
  )
238
247
 
239
248
  # Codespace created event, send once
240
- codespace_created_at = load_user_profile().get('codespace_created_at')
249
+ codespace_created_at = load_user_profile().get("codespace_created_at")
241
250
  if codespace_created_at is None:
242
- created_at = datetime.fromisoformat(codespace.get('created_at'))
243
- prop['state'] = 'created'
244
- _collector.log_event(prop, 'codespace_instance', event_triggered_at=created_at,
245
- user_properties=user_prop)
246
- update_user_profile({'codespace_created_at': codespace.get('created_at')})
251
+ created_at = datetime.fromisoformat(codespace.get("created_at"))
252
+ prop["state"] = "created"
253
+ _collector.log_event(prop, "codespace_instance", event_triggered_at=created_at, user_properties=user_prop)
254
+ update_user_profile({"codespace_created_at": codespace.get("created_at")})
247
255
 
248
256
  # Codespace available event, send multiple times as start/stop it
249
257
  available_at = get_github_codespace_available_at(codespace)
250
- if available_at and available_at.isoformat() != load_user_profile().get('codespace_available_at'):
251
- prop['state'] = 'available'
252
- _collector.log_event(prop, 'codespace_instance', event_triggered_at=available_at,
253
- user_properties=user_prop)
254
- update_user_profile({'codespace_available_at': available_at.isoformat()})
258
+ if available_at and available_at.isoformat() != load_user_profile().get("codespace_available_at"):
259
+ prop["state"] = "available"
260
+ _collector.log_event(prop, "codespace_instance", event_triggered_at=available_at, user_properties=user_prop)
261
+ update_user_profile({"codespace_available_at": available_at.isoformat()})
255
262
 
256
263
  # Codespace instance event should be flushed immediately
257
264
  _collector.send_events()
@@ -259,20 +266,25 @@ def log_codespaces_events(command):
259
266
 
260
267
  def log_single_env_event():
261
268
  prop = dict(
262
- action='launch_server',
269
+ action="launch_server",
263
270
  )
264
- log_event(prop, '[Experiment] single_environment')
271
+ log_event(prop, "[Experiment] single_environment")
265
272
  _collector.schedule_flush()
266
273
 
267
274
 
268
275
  def log_performance(feature_name: str, metrics: Dict):
269
276
  prop = metrics
270
- log_event(prop, f'[Performance] {feature_name}')
277
+ log_event(prop, f"[Performance] {feature_name}")
278
+ _collector.schedule_flush()
279
+
280
+
281
+ def log_connected_to_cloud():
282
+ log_event({"action": "connected_to_cloud"}, "Connect OSS to Cloud")
271
283
  _collector.schedule_flush()
272
284
 
273
285
 
274
286
  def capture_exception(e):
275
- user_id = load_user_profile().get('user_id')
287
+ user_id = load_user_profile().get("user_id")
276
288
  if is_ci_env() is True:
277
289
  user_id = f"{user_id}_CI"
278
290
 
recce/event/collector.py CHANGED
@@ -4,20 +4,19 @@ import platform
4
4
  import sys
5
5
  import time
6
6
  from contextlib import contextmanager
7
- from datetime import datetime
7
+ from datetime import datetime, timezone
8
8
  from json import JSONDecodeError
9
- from datetime import timezone
10
9
 
11
10
  import portalocker
12
11
  import requests
13
12
 
14
- from recce import __version__, is_ci_env
13
+ from recce import __version__, is_ci_env, is_recce_cloud_instance
15
14
  from recce.github import is_github_codespace
16
15
 
17
16
 
18
17
  class Collector:
19
18
  def __init__(self):
20
- self._api_endpoint = 'https://api.amplitude.com/2/httpapi'
19
+ self._api_endpoint = "https://api.amplitude.com/2/httpapi"
21
20
  self._api_key = None
22
21
  self._user_id = None
23
22
 
@@ -26,6 +25,7 @@ class Collector:
26
25
  self._upload_threshold = 10
27
26
  self._is_ci: bool = is_ci_env()
28
27
  self._is_github_codespace: bool = is_github_codespace()
28
+ self._is_recce_cloud_instance: bool = is_recce_cloud_instance()
29
29
  self._flush_timer = None
30
30
 
31
31
  def schedule_flush(self):
@@ -35,6 +35,7 @@ class Collector:
35
35
 
36
36
  # send async thread
37
37
  import threading
38
+
38
39
  if self._flush_timer:
39
40
  try:
40
41
  self._flush_timer.cancel()
@@ -59,11 +60,18 @@ class Collector:
59
60
  self._unsend_events_file = unsend_events_file
60
61
  self._check_required_files()
61
62
 
62
- def _log_event(self, user_id, event_type, created_at, user_properties, event_properties, ):
63
+ def _log_event(
64
+ self,
65
+ user_id,
66
+ event_type,
67
+ created_at,
68
+ user_properties,
69
+ event_properties,
70
+ ):
63
71
  event = dict(
64
72
  user_id=user_id,
65
73
  event_type=event_type,
66
- ip='$remote',
74
+ ip="$remote",
67
75
  time=int(time.mktime(created_at.timetuple())),
68
76
  user_properties=user_properties,
69
77
  event_properties=event_properties,
@@ -91,7 +99,7 @@ class Collector:
91
99
  else:
92
100
  # Convert to UTC timezone
93
101
  created_at = event_triggered_at.astimezone(timezone.utc)
94
- python_version = f'{sys.version_info.major}.{sys.version_info.minor}'
102
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
95
103
 
96
104
  # when the recce is running in automation use cases
97
105
  # replace the user id with project_id to avoid so many unique user id
@@ -102,6 +110,7 @@ class Collector:
102
110
  python_version=python_version,
103
111
  is_ci=self._is_ci,
104
112
  is_github_codespace=self._is_github_codespace,
113
+ is_recce_cloud_instance=self._is_recce_cloud_instance,
105
114
  )
106
115
 
107
116
  if user_properties is not None:
@@ -120,17 +129,18 @@ class Collector:
120
129
  if not os.path.exists(user_home):
121
130
  os.makedirs(user_home, exist_ok=True)
122
131
  if not os.path.exists(self._unsend_events_file):
123
- with portalocker.Lock(self._unsend_events_file, 'w+', timeout=5) as f:
124
- f.write(json.dumps({'unsend_events': []}))
132
+ with portalocker.Lock(self._unsend_events_file, "w+", timeout=5) as f:
133
+ f.write(json.dumps({"unsend_events": []}))
125
134
 
126
135
  def _is_full(self):
127
- with portalocker.Lock(self._unsend_events_file, 'r+', timeout=5) as f:
136
+ with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
128
137
  o = json.loads(f.read())
129
- return len(o.get('unsend_events', [])) >= self._upload_threshold
138
+ return len(o.get("unsend_events", [])) >= self._upload_threshold
130
139
 
131
140
  @contextmanager
132
141
  def load_json(self):
133
- with portalocker.Lock(self._unsend_events_file, 'r+', timeout=5) as f:
142
+ with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
143
+ o = None
134
144
  try:
135
145
  o = json.loads(f.read())
136
146
  yield o
@@ -140,15 +150,16 @@ class Collector:
140
150
  finally:
141
151
  f.seek(0)
142
152
  f.truncate()
143
- f.write(json.dumps(o))
153
+ if o is not None:
154
+ f.write(json.dumps(o))
144
155
 
145
156
  def send_events(self):
146
157
  with self.load_json() as o:
147
158
  payload = dict(
148
159
  api_key=self._api_key,
149
- events=o['unsend_events'],
160
+ events=o["unsend_events"],
150
161
  )
151
- o['unsend_events'] = []
162
+ o["unsend_events"] = []
152
163
  try:
153
164
  requests.post(self._api_endpoint, json=payload)
154
165
  except Exception:
@@ -157,17 +168,17 @@ class Collector:
157
168
 
158
169
  def _store_to_file(self, event):
159
170
  with self.load_json() as o:
160
- events = o.get('unsend_events', None)
171
+ events = o.get("unsend_events", None)
161
172
  if events is None:
162
- o['unsend_events'] = []
173
+ o["unsend_events"] = []
163
174
 
164
- o['unsend_events'].append(event)
175
+ o["unsend_events"].append(event)
165
176
 
166
177
  def _cleanup_unsend_events(self):
167
178
  with self.load_json() as o:
168
- events = o.get('unsend_events', None)
179
+ events = o.get("unsend_events", None)
169
180
  if events is None:
170
- o['unsend_events'] = []
181
+ o["unsend_events"] = []
171
182
 
172
- while len(o['unsend_events']) > self._delete_threshold:
173
- o['unsend_events'].pop(0)
183
+ while len(o["unsend_events"]) > self._delete_threshold:
184
+ o["unsend_events"].pop(0)
recce/event/track.py CHANGED
@@ -18,8 +18,8 @@ from recce.git import current_branch, hosting_repo
18
18
 
19
19
  console = Console()
20
20
 
21
- _enable_traceback: bool = os.environ.get('RECCE_PRINT_TRACEBACK') == '1'
22
- logger = logging.getLogger('uvicorn')
21
+ _enable_traceback: bool = os.environ.get("RECCE_PRINT_TRACEBACK") == "1"
22
+ logger = logging.getLogger("uvicorn")
23
23
 
24
24
 
25
25
  class TrackCommand(Command):
@@ -39,11 +39,23 @@ class TrackCommand(Command):
39
39
  deprecated: bool = False,
40
40
  beta: bool = False,
41
41
  ) -> None:
42
- super(TrackCommand, self).__init__(name, context_settings, callback, params, help, epilog, short_help,
43
- options_metavar, add_help_option, no_args_is_help, hidden, deprecated)
42
+ super(TrackCommand, self).__init__(
43
+ name,
44
+ context_settings,
45
+ callback,
46
+ params,
47
+ help,
48
+ epilog,
49
+ short_help,
50
+ options_metavar,
51
+ add_help_option,
52
+ no_args_is_help,
53
+ hidden,
54
+ deprecated,
55
+ )
44
56
 
45
57
  def _show_error_message(self, msg, params):
46
- if params.get('debug'):
58
+ if params.get("debug"):
47
59
  console.print_exception(show_locals=True)
48
60
  else:
49
61
  print(traceback.format_exc())
@@ -51,39 +63,39 @@ class TrackCommand(Command):
51
63
  # console.out(msg, highlight=False)
52
64
 
53
65
  def _show_hint_message(self, hint):
54
- console.print(f'[bold yellow]Hint[/bold yellow]:\n {escape(hint)}')
66
+ console.print(f"[bold yellow]Hint[/bold yellow]:\n {escape(hint)}")
55
67
 
56
68
  def invoke(self, ctx: Context) -> t.Any:
57
69
  status = False
58
70
  start_time = time.time()
59
- reason = 'error'
60
- event.set_exception_tag('command', ctx.command.name)
71
+ reason = "error"
72
+ event.set_exception_tag("command", ctx.command.name)
61
73
  event.log_codespaces_events(ctx.command.name)
62
74
 
63
75
  try:
64
76
  ret = super(TrackCommand, self).invoke(ctx)
65
77
  if ret is None or ret == 0:
66
78
  status = True
67
- reason = 'ok'
79
+ reason = "ok"
68
80
  else:
69
- reason = 'error'
81
+ reason = "error"
70
82
  sys.exit(ret)
71
83
  return ret
72
84
  except RecceException as e:
73
85
  logger.debug(traceback.format_exc())
74
86
  console.log("[Error] " + str(e))
75
- reason = 'error'
87
+ reason = "error"
76
88
  sys.exit(1)
77
89
  except SystemExit as e:
78
- reason = 'error'
90
+ reason = "error"
79
91
  raise e
80
92
  except KeyboardInterrupt as e:
81
- reason = 'aborted'
93
+ reason = "aborted"
82
94
  raise e
83
95
  except Exception as e:
84
96
  self._show_error_message(str(e), ctx.params)
85
97
  event.capture_exception(e)
86
- reason = 'fatal'
98
+ reason = "fatal"
87
99
  event.flush_exceptions()
88
100
  sys.exit(1)
89
101
  finally:
@@ -93,32 +105,32 @@ class TrackCommand(Command):
93
105
  branch = current_branch()
94
106
  command = ctx.command.name
95
107
  duration = end_time - start_time
96
- target_path = ctx.params.get('target_path', None)
97
- target_base_path = ctx.params.get('target_base_path', None)
108
+ target_path = ctx.params.get("target_path", None)
109
+ target_base_path = ctx.params.get("target_base_path", None)
98
110
  props = dict(
99
111
  command=command,
100
112
  status=status,
101
113
  reason=reason,
102
114
  duration=duration,
103
- cloud=ctx.params.get('cloud', False),
104
- review=ctx.params.get('review', False),
105
- debug=ctx.params.get('debug', False),
115
+ cloud=ctx.params.get("cloud", False),
116
+ review=ctx.params.get("review", False),
117
+ debug=ctx.params.get("debug", False),
106
118
  )
107
119
 
108
120
  if runner is not None:
109
- props['runner_type'] = runner
121
+ props["runner_type"] = runner
110
122
 
111
123
  if repo is not None:
112
- props['repository'] = sha256(repo.encode()).hexdigest()
124
+ props["repository"] = sha256(repo.encode()).hexdigest()
113
125
 
114
126
  if branch is not None:
115
- props['branch'] = sha256(branch.encode()).hexdigest()
127
+ props["branch"] = sha256(branch.encode()).hexdigest()
116
128
 
117
129
  if target_path is not None:
118
- props['target_path'] = sha256(target_path.encode()).hexdigest()
130
+ props["target_path"] = sha256(target_path.encode()).hexdigest()
119
131
 
120
132
  if target_base_path is not None:
121
- props['target_base_path'] = sha256(target_base_path.encode()).hexdigest()
133
+ props["target_base_path"] = sha256(target_base_path.encode()).hexdigest()
122
134
 
123
135
  try:
124
136
  recce_context = load_context()
@@ -128,9 +140,19 @@ class TrackCommand(Command):
128
140
 
129
141
  if recce_context is not None:
130
142
  if recce_context.adapter_type == "dbt":
131
- props['adapter_type'] = 'DBT'
143
+ props["adapter_type"] = "DBT"
144
+ # Add dbt warehouse type only for dbt adapter
145
+ try:
146
+ from recce.adapter.dbt_adapter import DbtAdapter
147
+
148
+ dbt_adapter: DbtAdapter = recce_context.adapter
149
+ warehouse_type = dbt_adapter.adapter.type()
150
+ props["warehouse_type"] = warehouse_type
151
+ except Exception:
152
+ # If we can't get the warehouse type, skip it
153
+ pass
132
154
  elif recce_context.adapter_type == "sqlmesh":
133
- props['adapter_type'] = 'SQLMesh'
155
+ props["adapter_type"] = "SQLMesh"
134
156
 
135
- event.log_event(props, 'command', params=ctx.params)
157
+ event.log_event(props, "command", params=ctx.params)
136
158
  event.flush_events()
recce/exceptions.py CHANGED
@@ -7,7 +7,7 @@ class RecceException(Exception):
7
7
 
8
8
  class RecceCancelException(RecceException):
9
9
  def __init__(self):
10
- super().__init__('Cancelled', False)
10
+ super().__init__("Cancelled", False)
11
11
 
12
12
 
13
13
  class RecceConfigException(RecceException):
recce/git.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import os
2
2
 
3
- from git import Repo, InvalidGitRepositoryError
3
+ from git import InvalidGitRepositoryError, Repo
4
4
 
5
5
 
6
6
  def current_default_branch():
7
7
  try:
8
8
  repo = Repo(search_parent_directories=True)
9
- return repo.remotes.origin.refs['HEAD'].reference.remote_head
9
+ return repo.remotes.origin.refs["HEAD"].reference.remote_head
10
10
  except Exception:
11
11
  return None
12
12
 
@@ -53,19 +53,19 @@ def commit_hash_from_branch(branch: str, short_length: int = 7, short: bool = Fa
53
53
  return None
54
54
 
55
55
 
56
- def hosting_repo(remote: str = 'origin'):
56
+ def hosting_repo(remote: str = "origin"):
57
57
  try:
58
58
  repo = Repo(search_parent_directories=True)
59
59
  origin_url = repo.remote(name=remote).url
60
60
  remote_repo = None
61
61
 
62
- if origin_url.startswith('git@'):
62
+ if origin_url.startswith("git@"):
63
63
  # Handle git@github.com:user/repo.git
64
- remote_repo = origin_url.split(':')[1].replace('.git', '')
64
+ remote_repo = origin_url.split(":")[1].replace(".git", "")
65
65
 
66
- elif origin_url.startswith('https://') or origin_url.startswith('http://'):
66
+ elif origin_url.startswith("https://") or origin_url.startswith("http://"):
67
67
  # Handle https://github.com/user/repo.git or http://github.com/user/repo.git
68
- remote_repo = '/'.join(origin_url.split('/')[-2:]).replace('.git', '')
68
+ remote_repo = "/".join(origin_url.split("/")[-2:]).replace(".git", "")
69
69
 
70
70
  return remote_repo
71
71
  except ValueError: